mirror of
https://github.com/GAM-team/GAM.git
synced 2025-06-06 00:07:21 +00:00
Upgrade to oauth2client 2.0.1. Remove support for seperate pem keys.
This commit is contained in:
parent
1d30eb7d91
commit
d8a78d96ae
BIN
src/GoogleUpdate.adm
Normal file
BIN
src/GoogleUpdate.adm
Normal file
Binary file not shown.
5453
src/fakeusers.newer
Normal file
5453
src/fakeusers.newer
Normal file
File diff suppressed because it is too large
Load Diff
25
src/gam.py
25
src/gam.py
@ -37,6 +37,7 @@ import googleapiclient.discovery
|
||||
import googleapiclient.errors
|
||||
import googleapiclient.http
|
||||
import oauth2client.client
|
||||
import oauth2client.service_account
|
||||
import oauth2client.file
|
||||
import oauth2client.tools
|
||||
import mimetypes
|
||||
@ -847,25 +848,11 @@ def buildGAPIServiceObject(api, act_as, soft_errors=False):
|
||||
printLine(GAM_WIKI_CREATE_CLIENT_SECRETS)
|
||||
systemErrorExit(6, None)
|
||||
json_data = json.loads(json_string)
|
||||
try:
|
||||
# new format with config and key in the .json file...
|
||||
GM_Globals[GM_OAUTH2SERVICE_ACCOUNT_EMAIL] = json_data[u'client_email']
|
||||
GM_Globals[GM_OAUTH2SERVICE_ACCOUNT_CLIENT_ID] = json_data[u'client_id']
|
||||
GM_Globals[GM_OAUTH2SERVICE_KEY] = json_data[u'private_key']
|
||||
except KeyError:
|
||||
try:
|
||||
# old format with config in the .json file and key in the .p12 file...
|
||||
GM_Globals[GM_OAUTH2SERVICE_ACCOUNT_EMAIL] = json_data[u'web'][u'client_email']
|
||||
GM_Globals[GM_OAUTH2SERVICE_ACCOUNT_CLIENT_ID] = json_data[u'web'][u'client_id']
|
||||
GM_Globals[GM_OAUTH2SERVICE_KEY] = readFile(GC_Values[GC_OAUTH2SERVICE_JSON].replace(u'.json', u'.p12'))
|
||||
except KeyError:
|
||||
printLine(MESSAGE_WIKI_INSTRUCTIONS_OAUTH2SERVICE_JSON)
|
||||
printLine(GAM_WIKI_CREATE_CLIENT_SECRETS)
|
||||
systemErrorExit(17, MESSAGE_OAUTH2SERVICE_JSON_INVALID.format(GC_Values[GC_OAUTH2SERVICE_JSON]))
|
||||
scope = getAPIScope(api)
|
||||
credentials = oauth2client.client.SignedJwtAssertionCredentials(GM_Globals[GM_OAUTH2SERVICE_ACCOUNT_EMAIL],
|
||||
GM_Globals[GM_OAUTH2SERVICE_KEY],
|
||||
scope=scope, user_agent=GAM_INFO, sub=act_as)
|
||||
scopes = getAPIScope(api)
|
||||
credentials = oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_dict(
|
||||
json_data, scopes)
|
||||
credentials = credentials.create_delegated(act_as)
|
||||
credentials.user_agent = GAM_INFO
|
||||
http = credentials.authorize(httplib2.Http(disable_ssl_certificate_validation=GC_Values[GC_NO_VERIFY_SSL],
|
||||
cache=GC_Values[GC_CACHE_DIR]))
|
||||
version = getAPIVer(api)
|
||||
|
375
src/gamtest.csv
Normal file
375
src/gamtest.csv
Normal file
@ -0,0 +1,375 @@
|
||||
AlaneEA@LeeSchools.Net
|
||||
superintendent@leeschools.net
|
||||
AdministrativeMeetings@LeeSchools.net
|
||||
EllenMA@LeeSchools.Net
|
||||
TonyAAl@LeeSchools.net
|
||||
CandaceMA@LeeSchools.Net
|
||||
Dwayne.Alton@leeschools.net
|
||||
MichaelLA@LeeSchools.net
|
||||
RondaKa@LeeSchools.Net
|
||||
ChevoneeseA@LeeSchools.net
|
||||
RebeccaAn@LeeSchools.net
|
||||
DaveJA@LeeSchools.net
|
||||
BenjaminIA@LeeSchools.Net
|
||||
AliceEB@LeeSchools.net
|
||||
RichardDB@LeeSchools.Net
|
||||
KimberlyBB@LeeSchools.net
|
||||
TroyAB@LeeSchools.net
|
||||
KevinB@LeeSchools.net
|
||||
DornB@LeeSchools.net
|
||||
PaulaMB@LeeSchools.Net
|
||||
MauraGB@LeeSchools.Net
|
||||
LindaKBe@leeschools.net
|
||||
MistyJB@LeeSchools.Net
|
||||
DanaAB@LeeSchools.net
|
||||
AndrewMB@LeeSchools.Net
|
||||
MaryABl@LeeSchools.net
|
||||
DwayneEB@LeeSchools.net
|
||||
AmyCB@LeeSchools.Net
|
||||
TaraLB@LeeSchools.net
|
||||
GeorgePB@leeschools.net
|
||||
StephanieTB@LeeSchools.net
|
||||
MelissaRB@LeeSchools.net
|
||||
DeniseKB@LeeSchools.Net
|
||||
TheresaMB@LeeSchools.Net
|
||||
TommyLB@LeeSchools.Net
|
||||
LindaKB@LeeSchools.Net
|
||||
MatthewTB@LeeSchools.net
|
||||
LoriMB@LeeSchools.Net
|
||||
KimberlyAB@LeeSchools.Net
|
||||
MonicaTB@LeeSchools.Net
|
||||
JamiMB@LeeSchools.Net
|
||||
AndrewSB@LeeSchools.net
|
||||
RobertGB@leeschools.net
|
||||
RebeccaAB@LeeSchools.Net
|
||||
JamesLB@LeeSchools.Net
|
||||
LindaEBu@LeeSchools.Net
|
||||
KristinTB@LeeSchools.Net
|
||||
MarshaAB@LeeSchools.Net
|
||||
DaveBu@LeeSchools.net
|
||||
GeorgeKB@LeeSchools.Net
|
||||
CarlCB@LeeSchools.Net
|
||||
ChristopherSB@LeeSchools.net
|
||||
MikeCB@LeeSchools.Net
|
||||
ChristineRB@LeeSchools.Net
|
||||
RobertLB@LeeSchools.Net
|
||||
LeighLB@LeeSchools.Net
|
||||
BrianFB@LeeSchools.net
|
||||
ChristopheRC@LeeSchools.Net
|
||||
TeriLC@LeeSchools.Net
|
||||
LindaMCa@LeeSchools.Net
|
||||
SusanLC@LeeSchools.Net
|
||||
DeniseMC@LeeSchools.net
|
||||
LorieAC@LeeSchools.Net
|
||||
MikeSC@LeeSchools.Net
|
||||
AnikaBC@leeschools.net
|
||||
EdwinLC@LeeSchools.Net
|
||||
BarbaraC@LeeSchools.net
|
||||
AmityAC@LeeSchools.net
|
||||
CyndiAC@LeeSchools.Net
|
||||
ChristineCh@LeeSchools.Net
|
||||
JenniferMCo@LeeSchools.net
|
||||
RobertLC@LeeSchools.net
|
||||
AnnFC@LeeSchools.net
|
||||
KimberlyACo@leeschools.net
|
||||
ElizabethACo@LeeSchools.Net
|
||||
ScottGC@LeeSchools.Net
|
||||
RobertFCo@LeeSchools.Net
|
||||
JackieLC@LeeSchools.Net
|
||||
MichelleCo@LeeSchools.Net
|
||||
LaurenCCo@LeeSchools.net
|
||||
DwayneMC@LeeSchools.Net
|
||||
JamiDC@LeeSchools.net
|
||||
MarilynJC@leeschools.net
|
||||
BrianLC@LeeSchools.Net
|
||||
AaronBC@LeeSchools.Net
|
||||
HelenJD@LeeSchools.net
|
||||
KennaJD@leeschools.net
|
||||
GeraldBD@LeeSchools.Net
|
||||
AllisonAD@LeeSchools.net
|
||||
AmiVD@LeeSchools.Net
|
||||
JenniferLD@LeeSchools.net
|
||||
LetaDD@LeeSchools.Net
|
||||
DebbieLDi@LeeSchools.Net
|
||||
JeffHD@LeeSchools.Net
|
||||
KennethDD@LeeSchools.net
|
||||
RobertDo@LeeSchools.net
|
||||
KatieAD@LeeSchools.Net
|
||||
JohnWD@LeeSchools.Net
|
||||
JessicaKD@LeeSchools.Net
|
||||
JuleeAD@LeeSchools.Net
|
||||
AlexJD@LeeSchools.Net
|
||||
LisaDE@LeeSchools.Net
|
||||
BrianE@LeeSchools.net
|
||||
TrentHE@LeeSchools.Net
|
||||
LynnME@LeeSchools.Net
|
||||
JenniferAE@LeeSchools.Net
|
||||
RitaEE@LeeSchools.net
|
||||
DarrylEE@LeeSchools.Net
|
||||
PattiDE@LeeSchools.Net
|
||||
ChristianJE@LeeSchools.Net
|
||||
DamanJE@LeeSchools.Net
|
||||
JeffreyBE@LeeSchools.Net
|
||||
AngelaE@LeeSchools.net
|
||||
ToddGE@LeeSchools.net
|
||||
AnnWF@LeeSchools.Net
|
||||
GinaVF@LeeSchools.net
|
||||
ElizabethFF@LeeSchools.Net
|
||||
JillianLF@LeeSchools.net
|
||||
DeniseCF@LeeSchools.Net
|
||||
ShannaMF@LeeSchools.Net
|
||||
JamesLF@LeeSchools.net
|
||||
JeananneVF@LeeSchools.Net
|
||||
TammyWF@LeeSchools.Net
|
||||
MichelleHF@LeeSchools.Net
|
||||
RobinRF@LeeSchools.Net
|
||||
CarolAF@LeeSchools.Net
|
||||
GwendelynYF@LeeSchools.net
|
||||
MichaelRGa@LeeSchools.Net
|
||||
ClintonRG@LeeSchools.net
|
||||
NenaSG@LeeSchools.Net
|
||||
BethAGe@LeeSchools.Net
|
||||
LawrenceMG@LeeSchools.Net
|
||||
CherryMG@LeeSchools.net
|
||||
BrianLG@LeeSchools.net
|
||||
DrVickiLG@LeeSchools.net
|
||||
MarthaDG@LeeSchools.Net
|
||||
RachelGO@LeeSchools.Net
|
||||
NancyJG@LeeSchools.net
|
||||
MaryBethBG@LeeSchools.Net
|
||||
MonicaLG@LeeSchools.net
|
||||
ErinBG@LeeSchools.Net
|
||||
SusannG@LeeSchools.net
|
||||
DaryaNG@LeeSchools.Net
|
||||
TanyaVG@LeeSchools.net
|
||||
LeslieAG@LeeSchools.Net
|
||||
AndreaLG@LeeSchools.net
|
||||
VivianCG@LeeSchools.Net
|
||||
DeadraLH@LeeSchools.net
|
||||
ShellieSH@LeeSchools.Net
|
||||
AndrewJH@LeeSchools.net
|
||||
StephenLH@LeeSchools.Net
|
||||
KariAHa@leeschools.net
|
||||
ToniLHa@LeeSchools.Net
|
||||
JohnnieMaeH@LeeSchools.net
|
||||
MarthaKH@LeeSchools.net
|
||||
PatrickJH@LeeSchools.Net
|
||||
BrandonJH@LeeSchools.net
|
||||
CynthiaMHe@LeeSchools.Net
|
||||
LeonaLH@LeeSchools.Net
|
||||
DonnieLH@LeeSchools.Net
|
||||
LoriMH@LeeSchools.Net
|
||||
DaleMH@LeeSchools.Net
|
||||
JimHOW@LeeSchools.Net
|
||||
BelindaJH@LeeSchools.Net
|
||||
DawnMHu@LeeSchools.net
|
||||
LeoPH@LeeSchools.Net
|
||||
JamesMI@LeeSchools.Net
|
||||
MarthaBI@LeeSchools.net
|
||||
RichardJI@LeeSchools.Net
|
||||
RyanJJ@LeeSchools.net
|
||||
SusanMJ@LeeSchools.Net
|
||||
JillEJ@LeeSchools.Net
|
||||
SusanLJ@LeeSchools.Net
|
||||
ArleneSK@LeeSchools.Net
|
||||
JanetLKe@LeeSchools.Net
|
||||
ShelleyAK@LeeSchools.Net
|
||||
FranK@LeeSchools.Net
|
||||
TerriMK@LeeSchools.net
|
||||
JamieKi@LeeSchools.net
|
||||
TonyaFK@LeeSchools.Net
|
||||
TimothyEK@LeeSchools.net
|
||||
AdamJK@LeeSchools.net
|
||||
JasonWKu@LeeSchools.net
|
||||
ChristyMK@LeeSchools.net
|
||||
ManeLa@LeeSchools.Net
|
||||
JeanneJL@LeeSchools.Net
|
||||
AshleyAL@LeeSchools.net
|
||||
DavidPL@LeeSchools.Net
|
||||
MelissaAL@LeeSchools.Net
|
||||
ElijahTL@LeeSchools.Net
|
||||
ScottLE@LeeSchools.net
|
||||
TamiML@LeeSchools.net
|
||||
NicoleLLe@LeeSchools.net
|
||||
KellyLE@LeeSchools.Net
|
||||
MichelleZL@LeeSchools.net
|
||||
RogerWL@LeeSchools.Net
|
||||
RuthieLL@LeeSchools.Net
|
||||
DianaJL@LeeSchools.Net
|
||||
EllenCL@LeeSchools.Net
|
||||
CharlesLU@LeeSchools.Net
|
||||
BrandyAM@LeeSchools.Net
|
||||
TomLM@LeeSchools.Net
|
||||
LindaMMA@LeeSchools.net
|
||||
SusanMMa@LeeSchools.net
|
||||
BrianMM@LeeSchools.Net
|
||||
KellyAMa@LeeSchools.Net
|
||||
KarenCM@LeeSchools.Net
|
||||
ShelleyMA@LeeSchools.Net
|
||||
JoyRM@leeschools.net
|
||||
KeithBM@LeeSchools.Net
|
||||
TamikaRM@LeeSchools.Net
|
||||
EdwardAM@LeeSchools.Net
|
||||
RobertWM@LeeSchools.Net
|
||||
JeniferMM@LeeSchools.net
|
||||
CindyKM@LeeSchools.Net
|
||||
BillBM@LeeSchools.Net
|
||||
JeffFM@LeeSchools.Net
|
||||
GeorgeM@LeeSchools.net
|
||||
NancyAM@LeeSchools.Net
|
||||
BonnieMc@LeeSchools.net
|
||||
DouglasIM@LeeSchools.Net
|
||||
AngelTM@LeeSchools.net
|
||||
MatthewCM@LeeSchools.Net
|
||||
MichelleSM@LeeSchools.Net
|
||||
StephanieME@LeeSchools.Net
|
||||
ThomasJM@LeeSchools.Net
|
||||
MatthewRMi@LeeSchools.Net
|
||||
ThomasCM@LeeSchools.Net
|
||||
JenniferLMi@leeschools.net
|
||||
JenniferJM@LeeSchools.net
|
||||
SamoneLM@LeeSchools.Net
|
||||
KimberleyAMo@LeeSchools.net
|
||||
RaniceM@LeeSchools.Net
|
||||
JodyJM@LeeSchools.net
|
||||
MarcBM@LeeSchools.Net
|
||||
ObedNM@LeeSchools.Net
|
||||
JamesWMo@LeeSchools.net
|
||||
RobertVM@LeeSchools.Net
|
||||
JacksonCM@LeeSchools.net
|
||||
RobertM@LeeSchools.Net
|
||||
ElysaM@LeeSchools.net
|
||||
LisaJM@LeeSchools.Net
|
||||
ShaneEM@LeeSchools.Net
|
||||
LeilaMM@LeeSchools.Net
|
||||
AngelaMN@LeeSchools.Net
|
||||
CarlosLN@LeeSchools.Net
|
||||
OscarOl@LeeSchools.net
|
||||
LauraNO@LeeSchools.Net
|
||||
NicoleDO@LeeSchools.Net
|
||||
RosaMP@LeeSchools.net
|
||||
DouglasCP@LeeSchools.Net
|
||||
HeatherVP@LeeSchools.net
|
||||
RichardWP@LeeSchools.Net
|
||||
JasonLP@LeeSchools.Net
|
||||
SusieLP@LeeSchools.net
|
||||
JohnMP@LeeSchools.Net
|
||||
DeniseDP@LeeSchools.Net
|
||||
JosephTP@LeeSchools.Net
|
||||
MitchellDPl@LeeSchools.net
|
||||
JasonBP@LeeSchools.Net
|
||||
AmandaMP@LeeSchools.net
|
||||
BrianTP@LeeSchools.net
|
||||
AngelaJP@LeeSchools.Net
|
||||
BethanyLQ@LeeSchools.net
|
||||
SorettaER@LeeSchools.net
|
||||
VictoriaTR@leeschools.net
|
||||
VirginiaRa@LeeSchools.net
|
||||
ScottCR@LeeSchools.Net
|
||||
JamieBR@LeeSchools.Net
|
||||
MaryER@LeeSchools.Net
|
||||
KristinaLR@leeschools.net
|
||||
JosephMR@LeeSchools.net
|
||||
GingerLR@LeeSchools.Net
|
||||
EvelynRI@LeeSchools.Net
|
||||
BrianWR@LeeSchools.net
|
||||
MelissaSR@LeeSchools.Net
|
||||
MicheleRO@LeeSchools.Net
|
||||
MaryLynnR@LeeSchools.net
|
||||
AngelaRR@LeeSchools.Net
|
||||
BrianneMR@LeeSchools.net
|
||||
AndreaHR@LeeSchools.net
|
||||
JohnCRo@LeeSchools.Net
|
||||
WilliamBR@LeeSchools.net
|
||||
AidaCS@LeeSchools.Net
|
||||
DianeTS@LeeSchools.Net
|
||||
LindaJSa@LeeSchools.Net
|
||||
JeffLSa@LeeSchools.Net
|
||||
AmandaLSa@LeeSchools.net
|
||||
DavidSan@LeeSchools.net
|
||||
MarkAS@LeeSchools.Net
|
||||
DouglasOS@LeeSchools.Net
|
||||
KennethAS@LeeSchools.Net
|
||||
RebeccaJS@LeeSchools.net
|
||||
AndreaS@LeeSchools.Net
|
||||
DonnaPS@LeeSchools.Net
|
||||
RonaldKS@LeeSchools.net
|
||||
CathyMS@LeeSchools.net
|
||||
TammyMS@LeeSchools.net
|
||||
CatherineHSc@leeschools.net
|
||||
JenniferAS@leeschools.net
|
||||
JenniferJS@LeeSchools.net
|
||||
NathanS@LeeSchools.net
|
||||
ValerieES@LeeSchools.Net
|
||||
DianeMS@LeeSchools.Net
|
||||
AlS@LeeSchools.Net
|
||||
JenniferIS@LeeSchools.Net
|
||||
JamesDS@LeeSchools.Net
|
||||
ChrisMS@LeeSchools.Net
|
||||
CindyFS@LeeSchools.Net
|
||||
TinaLSil@LeeSchools.net
|
||||
JasonSSi@LeeSchools.net
|
||||
ClaytonDS@LeeSchools.Net
|
||||
PeggyLS@leeschools.net
|
||||
LisaS@LeeSchools.net
|
||||
ShannonES@LeeSchools.Net
|
||||
hollylsm@LeeSchools.Net
|
||||
JenniferMSn@LeeSchools.Net
|
||||
ReginaldHS@leeschools.net
|
||||
MarlenS@LeeSchools.Net
|
||||
KarenSSp@LeeSchools.net
|
||||
JeffSS@LeeSchools.Net
|
||||
LauraMS@LeeSchools.Net
|
||||
MicheleASt@LeeSchools.net
|
||||
LauraRSt@LeeSchools.Net
|
||||
CayceLS@LeeSchools.Net
|
||||
KellyAST@LeeSchools.Net
|
||||
MikeSt@LeeSchools.net
|
||||
SonnyAS@LeeSchools.Net
|
||||
MaggieMS@LeeSchools.Net
|
||||
AmyS@LeeSchools.net
|
||||
CrissyWS@LeeSchools.Net
|
||||
RobLS@LeeSchools.Net
|
||||
KristineASu@LeeSchools.Net
|
||||
ShellieDT@LeeSchools.Net
|
||||
KellyVT@LeeSchools.net
|
||||
MathewJT@LeeSchools.net
|
||||
MirtaST@LeeSchools.Net
|
||||
DonaldWT@LeeSchools.Net
|
||||
CheriseWT@LeeSchools.Net
|
||||
MelixsaT@LeeSchools.Net
|
||||
LauraAT@LeeSchools.Net
|
||||
WilliamBT@LeeSchools.net
|
||||
JackieBT@LeeSchools.Net
|
||||
KimberlyAV@LeeSchools.Net
|
||||
MaraKV@LeeSchools.Net
|
||||
ElizabethAVi@LeeSchools.net
|
||||
CharlesRV@LeeSchools.net
|
||||
BarbaraVO@LeeSchools.Net
|
||||
BarbaraAWa@LeeSchools.Net
|
||||
ToniMW@LeeSchools.Net
|
||||
ForrestWa@LeeSchools.Net
|
||||
KennethCW@LeeSchools.Net
|
||||
EricLW@LeeSchools.Net
|
||||
AmyEW@LeeSchools.net
|
||||
NeketaCW@LeeSchools.Net
|
||||
PadraicFW@LeeSchools.net
|
||||
RobertaAW@LeeSchools.Net
|
||||
DottieJW@LeeSchools.Net
|
||||
JenniferKW@LeeSchools.net
|
||||
HermanDW@LeeSchools.Net
|
||||
ErinRW@LeeSchools.Net
|
||||
JoeWil@LeeSchools.net
|
||||
JeremeEW@LeeSchools.Net
|
||||
EmmaMW@LeeSchools.net
|
||||
StaciaAW@LeeSchools.net
|
||||
SherriMW@LeeSchools.Net
|
||||
CarolAW@LeeSchools.Net
|
||||
KarenLWo@LeeSchools.Net
|
||||
LarryNW@LeeSchools.Net
|
||||
WinstonW@LeeSchools.net
|
||||
LisaMWr@leeschools.net
|
||||
MarlaSW@LeeSchools.Net
|
||||
RitaAZ@LeeSchools.net
|
||||
SusanKZ@LeeSchools.Net
|
||||
TraceySZ@LeeSchools.Net
|
|
BIN
src/googlechromestandaloneenterprise64.msi
Normal file
BIN
src/googlechromestandaloneenterprise64.msi
Normal file
Binary file not shown.
BIN
src/googleupdateadmx/GoogleUpdateAdmx/GoogleUpdate.admx
Normal file
BIN
src/googleupdateadmx/GoogleUpdateAdmx/GoogleUpdate.admx
Normal file
Binary file not shown.
BIN
src/googleupdateadmx/GoogleUpdateAdmx/en-US/GoogleUpdate.adml
Normal file
BIN
src/googleupdateadmx/GoogleUpdateAdmx/en-US/GoogleUpdate.adml
Normal file
Binary file not shown.
@ -14,10 +14,10 @@
|
||||
|
||||
"""Client library for using OAuth2, especially with Google APIs."""
|
||||
|
||||
__version__ = '1.5.1'
|
||||
__version__ = '2.0.1'
|
||||
|
||||
GOOGLE_AUTH_URI = 'https://accounts.google.com/o/oauth2/auth'
|
||||
GOOGLE_AUTH_URI = 'https://accounts.google.com/o/oauth2/v2/auth'
|
||||
GOOGLE_DEVICE_URI = 'https://accounts.google.com/o/oauth2/device/code'
|
||||
GOOGLE_REVOKE_URI = 'https://accounts.google.com/o/oauth2/revoke'
|
||||
GOOGLE_TOKEN_URI = 'https://accounts.google.com/o/oauth2/token'
|
||||
GOOGLE_TOKEN_INFO_URI = 'https://www.googleapis.com/oauth2/v2/tokeninfo'
|
||||
GOOGLE_TOKEN_URI = 'https://www.googleapis.com/oauth2/v4/token'
|
||||
GOOGLE_TOKEN_INFO_URI = 'https://www.googleapis.com/oauth2/v3/tokeninfo'
|
||||
|
@ -14,7 +14,7 @@
|
||||
"""OpenSSL Crypto-related routines for oauth2client."""
|
||||
|
||||
import base64
|
||||
import six
|
||||
|
||||
from OpenSSL import crypto
|
||||
|
||||
from oauth2client._helpers import _parse_pem_key
|
||||
@ -68,6 +68,7 @@ class OpenSSLVerifier(object):
|
||||
Raises:
|
||||
OpenSSL.crypto.Error: if the key_pem can't be parsed.
|
||||
"""
|
||||
key_pem = _to_bytes(key_pem)
|
||||
if is_x509_cert:
|
||||
pubkey = crypto.load_certificate(crypto.FILETYPE_PEM, key_pem)
|
||||
else:
|
||||
@ -112,6 +113,7 @@ class OpenSSLSigner(object):
|
||||
Raises:
|
||||
OpenSSL.crypto.Error if the key can't be parsed.
|
||||
"""
|
||||
key = _to_bytes(key)
|
||||
parsed_pem_key = _parse_pem_key(key)
|
||||
if parsed_pem_key:
|
||||
pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, parsed_pem_key)
|
||||
@ -121,19 +123,17 @@ class OpenSSLSigner(object):
|
||||
return OpenSSLSigner(pkey)
|
||||
|
||||
|
||||
def pkcs12_key_as_pem(private_key_text, private_key_password):
|
||||
"""Convert the contents of a PKCS12 key to PEM using OpenSSL.
|
||||
def pkcs12_key_as_pem(private_key_bytes, private_key_password):
|
||||
"""Convert the contents of a PKCS#12 key to PEM using pyOpenSSL.
|
||||
|
||||
Args:
|
||||
private_key_text: String. Private key.
|
||||
private_key_password: String. Password for PKCS12.
|
||||
private_key_bytes: Bytes. PKCS#12 key in DER format.
|
||||
private_key_password: String. Password for PKCS#12 key.
|
||||
|
||||
Returns:
|
||||
String. PEM contents of ``private_key_text``.
|
||||
String. PEM contents of ``private_key_bytes``.
|
||||
"""
|
||||
decoded_body = base64.b64decode(private_key_text)
|
||||
private_key_password = _to_bytes(private_key_password)
|
||||
|
||||
pkcs12 = crypto.load_pkcs12(decoded_body, private_key_password)
|
||||
pkcs12 = crypto.load_pkcs12(private_key_bytes, private_key_password)
|
||||
return crypto.dump_privatekey(crypto.FILETYPE_PEM,
|
||||
pkcs12.get_privatekey())
|
||||
|
185
src/oauth2client/_pure_python_crypt.py
Normal file
185
src/oauth2client/_pure_python_crypt.py
Normal file
@ -0,0 +1,185 @@
|
||||
# Copyright 2016 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Pure Python crypto-related routines for oauth2client.
|
||||
|
||||
Uses the ``rsa``, ``pyasn1`` and ``pyasn1_modules`` packages
|
||||
to parse PEM files storing PKCS#1 or PKCS#8 keys as well as
|
||||
certificates.
|
||||
"""
|
||||
|
||||
from pyasn1.codec.der import decoder
|
||||
from pyasn1_modules import pem
|
||||
from pyasn1_modules.rfc2459 import Certificate
|
||||
from pyasn1_modules.rfc5208 import PrivateKeyInfo
|
||||
import rsa
|
||||
import six
|
||||
|
||||
from oauth2client._helpers import _from_bytes
|
||||
from oauth2client._helpers import _to_bytes
|
||||
|
||||
|
||||
_PKCS12_ERROR = r"""\
|
||||
PKCS12 format is not supported by the RSA library.
|
||||
Either install PyOpenSSL, or please convert .p12 format
|
||||
to .pem format:
|
||||
$ cat key.p12 | \
|
||||
> openssl pkcs12 -nodes -nocerts -passin pass:notasecret | \
|
||||
> openssl rsa > key.pem
|
||||
"""
|
||||
|
||||
_POW2 = (128, 64, 32, 16, 8, 4, 2, 1)
|
||||
_PKCS1_MARKER = ('-----BEGIN RSA PRIVATE KEY-----',
|
||||
'-----END RSA PRIVATE KEY-----')
|
||||
_PKCS8_MARKER = ('-----BEGIN PRIVATE KEY-----',
|
||||
'-----END PRIVATE KEY-----')
|
||||
_PKCS8_SPEC = PrivateKeyInfo()
|
||||
|
||||
|
||||
def _bit_list_to_bytes(bit_list):
|
||||
"""Converts an iterable of 1's and 0's to bytes.
|
||||
|
||||
Combines the list 8 at a time, treating each group of 8 bits
|
||||
as a single byte.
|
||||
"""
|
||||
num_bits = len(bit_list)
|
||||
byte_vals = bytearray()
|
||||
for start in six.moves.xrange(0, num_bits, 8):
|
||||
curr_bits = bit_list[start:start + 8]
|
||||
char_val = sum(val * digit
|
||||
for val, digit in zip(_POW2, curr_bits))
|
||||
byte_vals.append(char_val)
|
||||
return bytes(byte_vals)
|
||||
|
||||
|
||||
class RsaVerifier(object):
|
||||
"""Verifies the signature on a message.
|
||||
|
||||
Args:
|
||||
pubkey: rsa.key.PublicKey (or equiv), The public key to verify with.
|
||||
"""
|
||||
|
||||
def __init__(self, pubkey):
|
||||
self._pubkey = pubkey
|
||||
|
||||
def verify(self, message, signature):
|
||||
"""Verifies a message against a signature.
|
||||
|
||||
Args:
|
||||
message: string or bytes, The message to verify. If string, will be
|
||||
encoded to bytes as utf-8.
|
||||
signature: string or bytes, The signature on the message. If
|
||||
string, will be encoded to bytes as utf-8.
|
||||
|
||||
Returns:
|
||||
True if message was signed by the private key associated with the
|
||||
public key that this object was constructed with.
|
||||
"""
|
||||
message = _to_bytes(message, encoding='utf-8')
|
||||
try:
|
||||
return rsa.pkcs1.verify(message, signature, self._pubkey)
|
||||
except (ValueError, rsa.pkcs1.VerificationError):
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, key_pem, is_x509_cert):
|
||||
"""Construct an RsaVerifier instance from a string.
|
||||
|
||||
Args:
|
||||
key_pem: string, public key in PEM format.
|
||||
is_x509_cert: bool, True if key_pem is an X509 cert, otherwise it
|
||||
is expected to be an RSA key in PEM format.
|
||||
|
||||
Returns:
|
||||
RsaVerifier instance.
|
||||
|
||||
Raises:
|
||||
ValueError: if the key_pem can't be parsed. In either case, error
|
||||
will begin with 'No PEM start marker'. If
|
||||
``is_x509_cert`` is True, will fail to find the
|
||||
"-----BEGIN CERTIFICATE-----" error, otherwise fails
|
||||
to find "-----BEGIN RSA PUBLIC KEY-----".
|
||||
"""
|
||||
key_pem = _to_bytes(key_pem)
|
||||
if is_x509_cert:
|
||||
der = rsa.pem.load_pem(key_pem, 'CERTIFICATE')
|
||||
asn1_cert, remaining = decoder.decode(der, asn1Spec=Certificate())
|
||||
if remaining != b'':
|
||||
raise ValueError('Unused bytes', remaining)
|
||||
|
||||
cert_info = asn1_cert['tbsCertificate']['subjectPublicKeyInfo']
|
||||
key_bytes = _bit_list_to_bytes(cert_info['subjectPublicKey'])
|
||||
pubkey = rsa.PublicKey.load_pkcs1(key_bytes, 'DER')
|
||||
else:
|
||||
pubkey = rsa.PublicKey.load_pkcs1(key_pem, 'PEM')
|
||||
return cls(pubkey)
|
||||
|
||||
|
||||
class RsaSigner(object):
|
||||
"""Signs messages with a private key.
|
||||
|
||||
Args:
|
||||
pkey: rsa.key.PrivateKey (or equiv), The private key to sign with.
|
||||
"""
|
||||
|
||||
def __init__(self, pkey):
|
||||
self._key = pkey
|
||||
|
||||
def sign(self, message):
|
||||
"""Signs a message.
|
||||
|
||||
Args:
|
||||
message: bytes, Message to be signed.
|
||||
|
||||
Returns:
|
||||
string, The signature of the message for the given key.
|
||||
"""
|
||||
message = _to_bytes(message, encoding='utf-8')
|
||||
return rsa.pkcs1.sign(message, self._key, 'SHA-256')
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, key, password='notasecret'):
|
||||
"""Construct an RsaSigner instance from a string.
|
||||
|
||||
Args:
|
||||
key: string, private key in PEM format.
|
||||
password: string, password for private key file. Unused for PEM
|
||||
files.
|
||||
|
||||
Returns:
|
||||
RsaSigner instance.
|
||||
|
||||
Raises:
|
||||
ValueError if the key cannot be parsed as PKCS#1 or PKCS#8 in
|
||||
PEM format.
|
||||
"""
|
||||
key = _from_bytes(key) # pem expects str in Py3
|
||||
marker_id, key_bytes = pem.readPemBlocksFromFile(
|
||||
six.StringIO(key), _PKCS1_MARKER, _PKCS8_MARKER)
|
||||
|
||||
if marker_id == 0:
|
||||
pkey = rsa.key.PrivateKey.load_pkcs1(key_bytes,
|
||||
format='DER')
|
||||
elif marker_id == 1:
|
||||
key_info, remaining = decoder.decode(
|
||||
key_bytes, asn1Spec=_PKCS8_SPEC)
|
||||
if remaining != b'':
|
||||
raise ValueError('Unused bytes', remaining)
|
||||
pkey_info = key_info.getComponentByName('privateKey')
|
||||
pkey = rsa.key.PrivateKey.load_pkcs1(pkey_info.asOctets(),
|
||||
format='DER')
|
||||
else:
|
||||
raise ValueError('No key could be detected.')
|
||||
|
||||
return cls(pkey)
|
@ -17,7 +17,6 @@ from Crypto.PublicKey import RSA
|
||||
from Crypto.Hash import SHA256
|
||||
from Crypto.Signature import PKCS1_v1_5
|
||||
from Crypto.Util.asn1 import DerSequence
|
||||
import six
|
||||
|
||||
from oauth2client._helpers import _parse_pem_key
|
||||
from oauth2client._helpers import _to_bytes
|
||||
@ -116,14 +115,12 @@ class PyCryptoSigner(object):
|
||||
Raises:
|
||||
NotImplementedError if the key isn't in PEM format.
|
||||
"""
|
||||
parsed_pem_key = _parse_pem_key(key)
|
||||
parsed_pem_key = _parse_pem_key(_to_bytes(key))
|
||||
if parsed_pem_key:
|
||||
pkey = RSA.importKey(parsed_pem_key)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
'PKCS12 format is not supported by the PyCrypto library. '
|
||||
'Try converting to a "PEM" '
|
||||
'(openssl pkcs12 -in xxxxx.p12 -nodes -nocerts > '
|
||||
'privatekey.pem) '
|
||||
'or using PyOpenSSL if native code is an option.')
|
||||
'No key in PEM format was detected. This implementation '
|
||||
'can only use the PyCrypto library for keys in PEM '
|
||||
'format.')
|
||||
return PyCryptoSigner(pkey)
|
||||
|
@ -30,6 +30,7 @@ import tempfile
|
||||
import time
|
||||
import shutil
|
||||
import six
|
||||
from six.moves import http_client
|
||||
from six.moves import urllib
|
||||
|
||||
import httplib2
|
||||
@ -73,7 +74,7 @@ ID_TOKEN_VERIFICATON_CERTS = ID_TOKEN_VERIFICATION_CERTS
|
||||
OOB_CALLBACK_URN = 'urn:ietf:wg:oauth:2.0:oob'
|
||||
|
||||
# Google Data client libraries may need to set this to [401, 403].
|
||||
REFRESH_STATUS_CODES = [401]
|
||||
REFRESH_STATUS_CODES = (http_client.UNAUTHORIZED,)
|
||||
|
||||
# The value representing user credentials.
|
||||
AUTHORIZED_USER = 'authorized_user'
|
||||
@ -101,6 +102,8 @@ ADC_HELP_MSG = (
|
||||
'https://developers.google.com/accounts/docs/'
|
||||
'application-default-credentials for more information.')
|
||||
|
||||
_WELL_KNOWN_CREDENTIALS_FILE = 'application_default_credentials.json'
|
||||
|
||||
# The access token along with the seconds in which it expires.
|
||||
AccessTokenInfo = collections.namedtuple(
|
||||
'AccessTokenInfo', ['access_token', 'expires_in'])
|
||||
@ -115,6 +118,10 @@ _GCE_METADATA_HOST = '169.254.169.254'
|
||||
_METADATA_FLAVOR_HEADER = 'Metadata-Flavor'
|
||||
_DESIRED_METADATA_FLAVOR = 'Google'
|
||||
|
||||
# Expose utcnow() at module level to allow for
|
||||
# easier testing (by replacing with a stub).
|
||||
_UTCNOW = datetime.datetime.utcnow
|
||||
|
||||
|
||||
class SETTINGS(object):
|
||||
"""Settings namespace for globally defined values."""
|
||||
@ -133,6 +140,13 @@ class AccessTokenRefreshError(Error):
|
||||
"""Error trying to refresh an expired access token."""
|
||||
|
||||
|
||||
class HttpAccessTokenRefreshError(AccessTokenRefreshError):
|
||||
"""Error (with HTTP status) trying to refresh an expired access token."""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(HttpAccessTokenRefreshError, self).__init__(*args)
|
||||
self.status = kwargs.get('status')
|
||||
|
||||
|
||||
class TokenRevokeError(Error):
|
||||
"""Error trying to revoke a token."""
|
||||
|
||||
@ -185,6 +199,13 @@ class MemoryCache(object):
|
||||
self.cache.pop(key, None)
|
||||
|
||||
|
||||
def _parse_expiry(expiry):
|
||||
if expiry and isinstance(expiry, datetime.datetime):
|
||||
return expiry.strftime(EXPIRY_FORMAT)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
class Credentials(object):
|
||||
"""Base class for all Credentials objects.
|
||||
|
||||
@ -195,7 +216,7 @@ class Credentials(object):
|
||||
JSON string as input and returns an instantiated Credentials object.
|
||||
"""
|
||||
|
||||
NON_SERIALIZED_MEMBERS = ['store']
|
||||
NON_SERIALIZED_MEMBERS = frozenset(['store'])
|
||||
|
||||
def authorize(self, http):
|
||||
"""Take an httplib2.Http instance (or equivalent) and authorizes it.
|
||||
@ -236,34 +257,37 @@ class Credentials(object):
|
||||
"""
|
||||
_abstract()
|
||||
|
||||
def _to_json(self, strip):
|
||||
def _to_json(self, strip, to_serialize=None):
|
||||
"""Utility function that creates JSON repr. of a Credentials object.
|
||||
|
||||
Args:
|
||||
strip: array, An array of names of members to not include in the
|
||||
strip: array, An array of names of members to exclude from the
|
||||
JSON.
|
||||
to_serialize: dict, (Optional) The properties for this object
|
||||
that will be serialized. This allows callers to modify
|
||||
before serializing.
|
||||
|
||||
Returns:
|
||||
string, a JSON representation of this instance, suitable to pass to
|
||||
from_json().
|
||||
"""
|
||||
t = type(self)
|
||||
d = copy.copy(self.__dict__)
|
||||
curr_type = self.__class__
|
||||
if to_serialize is None:
|
||||
to_serialize = copy.copy(self.__dict__)
|
||||
for member in strip:
|
||||
if member in d:
|
||||
del d[member]
|
||||
if (d.get('token_expiry') and
|
||||
isinstance(d['token_expiry'], datetime.datetime)):
|
||||
d['token_expiry'] = d['token_expiry'].strftime(EXPIRY_FORMAT)
|
||||
# Add in information we will need later to reconsistitue this instance.
|
||||
d['_class'] = t.__name__
|
||||
d['_module'] = t.__module__
|
||||
for key, val in d.items():
|
||||
if member in to_serialize:
|
||||
del to_serialize[member]
|
||||
to_serialize['token_expiry'] = _parse_expiry(
|
||||
to_serialize.get('token_expiry'))
|
||||
# Add in information we will need later to reconstitute this instance.
|
||||
to_serialize['_class'] = curr_type.__name__
|
||||
to_serialize['_module'] = curr_type.__module__
|
||||
for key, val in to_serialize.items():
|
||||
if isinstance(val, bytes):
|
||||
d[key] = val.decode('utf-8')
|
||||
to_serialize[key] = val.decode('utf-8')
|
||||
if isinstance(val, set):
|
||||
d[key] = list(val)
|
||||
return json.dumps(d)
|
||||
to_serialize[key] = list(val)
|
||||
return json.dumps(to_serialize)
|
||||
|
||||
def to_json(self):
|
||||
"""Creating a JSON representation of an instance of Credentials.
|
||||
@ -272,23 +296,23 @@ class Credentials(object):
|
||||
string, a JSON representation of this instance, suitable to pass to
|
||||
from_json().
|
||||
"""
|
||||
return self._to_json(Credentials.NON_SERIALIZED_MEMBERS)
|
||||
return self._to_json(self.NON_SERIALIZED_MEMBERS)
|
||||
|
||||
@classmethod
|
||||
def new_from_json(cls, s):
|
||||
def new_from_json(cls, json_data):
|
||||
"""Utility class method to instantiate a Credentials subclass from JSON.
|
||||
|
||||
Expects the JSON string to have been produced by to_json().
|
||||
|
||||
Args:
|
||||
s: string or bytes, JSON from to_json().
|
||||
json_data: string or bytes, JSON from to_json().
|
||||
|
||||
Returns:
|
||||
An instance of the subclass of Credentials that was serialized with
|
||||
to_json().
|
||||
"""
|
||||
json_string_as_unicode = _from_bytes(s)
|
||||
data = json.loads(json_string_as_unicode)
|
||||
json_data_as_unicode = _from_bytes(json_data)
|
||||
data = json.loads(json_data_as_unicode)
|
||||
# Find and call the right classmethod from_json() to restore
|
||||
# the object.
|
||||
module_name = data['_module']
|
||||
@ -303,8 +327,7 @@ class Credentials(object):
|
||||
module_obj = __import__(module_name,
|
||||
fromlist=module_name.split('.')[:-1])
|
||||
kls = getattr(module_obj, data['_class'])
|
||||
from_json = getattr(kls, 'from_json')
|
||||
return from_json(json_string_as_unicode)
|
||||
return kls.from_json(json_data_as_unicode)
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, unused_data):
|
||||
@ -333,21 +356,31 @@ class Storage(object):
|
||||
such that multiple processes and threads can operate on a single
|
||||
store.
|
||||
"""
|
||||
def __init__(self, lock=None):
|
||||
"""Create a Storage instance.
|
||||
|
||||
Args:
|
||||
lock: An optional threading.Lock-like object. Must implement at
|
||||
least acquire() and release(). Does not need to be re-entrant.
|
||||
"""
|
||||
self._lock = lock
|
||||
|
||||
def acquire_lock(self):
|
||||
"""Acquires any lock necessary to access this Storage.
|
||||
|
||||
This lock is not reentrant.
|
||||
"""
|
||||
pass
|
||||
if self._lock is not None:
|
||||
self._lock.acquire()
|
||||
|
||||
def release_lock(self):
|
||||
"""Release the Storage lock.
|
||||
|
||||
Trying to release a lock that isn't held will result in a
|
||||
RuntimeError.
|
||||
RuntimeError in the case of a threading.Lock or multiprocessing.Lock.
|
||||
"""
|
||||
pass
|
||||
if self._lock is not None:
|
||||
self._lock.release()
|
||||
|
||||
def locked_get(self):
|
||||
"""Retrieve credential.
|
||||
@ -676,23 +709,19 @@ class OAuth2Credentials(Credentials):
|
||||
self._retrieve_scopes(http.request)
|
||||
return self.scopes
|
||||
|
||||
def to_json(self):
|
||||
return self._to_json(Credentials.NON_SERIALIZED_MEMBERS)
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, s):
|
||||
def from_json(cls, json_data):
|
||||
"""Instantiate a Credentials object from a JSON description of it.
|
||||
|
||||
The JSON should have been produced by calling .to_json() on the object.
|
||||
|
||||
Args:
|
||||
data: dict, A deserialized JSON object.
|
||||
json_data: string or bytes, JSON to deserialize.
|
||||
|
||||
Returns:
|
||||
An instance of a Credentials subclass.
|
||||
"""
|
||||
s = _from_bytes(s)
|
||||
data = json.loads(s)
|
||||
data = json.loads(_from_bytes(json_data))
|
||||
if (data.get('token_expiry') and
|
||||
not isinstance(data['token_expiry'], datetime.datetime)):
|
||||
try:
|
||||
@ -728,7 +757,7 @@ class OAuth2Credentials(Credentials):
|
||||
if not self.token_expiry:
|
||||
return False
|
||||
|
||||
now = datetime.datetime.utcnow()
|
||||
now = _UTCNOW()
|
||||
if now >= self.token_expiry:
|
||||
logger.info('access_token is expired. Now: %s, token_expiry: %s',
|
||||
now, self.token_expiry)
|
||||
@ -771,7 +800,7 @@ class OAuth2Credentials(Credentials):
|
||||
valid; we just don't know anything about it.
|
||||
"""
|
||||
if self.token_expiry:
|
||||
now = datetime.datetime.utcnow()
|
||||
now = _UTCNOW()
|
||||
if self.token_expiry > now:
|
||||
time_delta = self.token_expiry - now
|
||||
# TODO(orestica): return time_delta.total_seconds()
|
||||
@ -829,7 +858,7 @@ class OAuth2Credentials(Credentials):
|
||||
refresh request.
|
||||
|
||||
Raises:
|
||||
AccessTokenRefreshError: When the refresh fails.
|
||||
HttpAccessTokenRefreshError: When the refresh fails.
|
||||
"""
|
||||
if not self.store:
|
||||
self._do_refresh_request(http_request)
|
||||
@ -857,7 +886,7 @@ class OAuth2Credentials(Credentials):
|
||||
refresh request.
|
||||
|
||||
Raises:
|
||||
AccessTokenRefreshError: When the refresh fails.
|
||||
HttpAccessTokenRefreshError: When the refresh fails.
|
||||
"""
|
||||
body = self._generate_refresh_request_body()
|
||||
headers = self._generate_refresh_request_headers()
|
||||
@ -866,16 +895,20 @@ class OAuth2Credentials(Credentials):
|
||||
resp, content = http_request(
|
||||
self.token_uri, method='POST', body=body, headers=headers)
|
||||
content = _from_bytes(content)
|
||||
if resp.status == 200:
|
||||
if resp.status == http_client.OK:
|
||||
d = json.loads(content)
|
||||
self.token_response = d
|
||||
self.access_token = d['access_token']
|
||||
self.refresh_token = d.get('refresh_token', self.refresh_token)
|
||||
if 'expires_in' in d:
|
||||
self.token_expiry = datetime.timedelta(
|
||||
seconds=int(d['expires_in'])) + datetime.datetime.utcnow()
|
||||
delta = datetime.timedelta(seconds=int(d['expires_in']))
|
||||
self.token_expiry = delta + _UTCNOW()
|
||||
else:
|
||||
self.token_expiry = None
|
||||
if 'id_token' in d:
|
||||
self.id_token = _extract_id_token(d['id_token'])
|
||||
else:
|
||||
self.id_token = None
|
||||
# On temporary refresh errors, the user does not actually have to
|
||||
# re-authorize, so we unflag here.
|
||||
self.invalid = False
|
||||
@ -897,7 +930,7 @@ class OAuth2Credentials(Credentials):
|
||||
self.store.locked_put(self)
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
raise AccessTokenRefreshError(error_msg)
|
||||
raise HttpAccessTokenRefreshError(error_msg, status=resp.status)
|
||||
|
||||
def _revoke(self, http_request):
|
||||
"""Revokes this credential and deletes the stored copy (if it exists).
|
||||
@ -927,7 +960,7 @@ class OAuth2Credentials(Credentials):
|
||||
query_params = {'token': token}
|
||||
token_revoke_uri = _update_query_params(self.revoke_uri, query_params)
|
||||
resp, content = http_request(token_revoke_uri)
|
||||
if resp.status == 200:
|
||||
if resp.status == http_client.OK:
|
||||
self.invalid = True
|
||||
else:
|
||||
error_msg = 'Invalid response %s.' % resp.status
|
||||
@ -972,7 +1005,7 @@ class OAuth2Credentials(Credentials):
|
||||
query_params)
|
||||
resp, content = http_request(token_info_uri)
|
||||
content = _from_bytes(content)
|
||||
if resp.status == 200:
|
||||
if resp.status == http_client.OK:
|
||||
d = json.loads(content)
|
||||
self.scopes = set(util.string_to_scopes(d.get('scope', '')))
|
||||
else:
|
||||
@ -1036,8 +1069,8 @@ class AccessTokenCredentials(OAuth2Credentials):
|
||||
revoke_uri=revoke_uri)
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, s):
|
||||
data = json.loads(_from_bytes(s))
|
||||
def from_json(cls, json_data):
|
||||
data = json.loads(_from_bytes(json_data))
|
||||
retval = AccessTokenCredentials(
|
||||
data['access_token'],
|
||||
data['user_agent'])
|
||||
@ -1078,7 +1111,7 @@ def _detect_gce_environment():
|
||||
headers = {_METADATA_FLAVOR_HEADER: _DESIRED_METADATA_FLAVOR}
|
||||
connection.request('GET', '/', headers=headers)
|
||||
response = connection.getresponse()
|
||||
if response.status == 200:
|
||||
if response.status == http_client.OK:
|
||||
return (response.getheader(_METADATA_FLAVOR_HEADER) ==
|
||||
_DESIRED_METADATA_FLAVOR)
|
||||
except socket.error: # socket.timeout or socket.error(64, 'Host is down')
|
||||
@ -1098,7 +1131,10 @@ def _in_gae_environment():
|
||||
return SETTINGS.env_name in ('GAE_PRODUCTION', 'GAE_LOCAL')
|
||||
|
||||
try:
|
||||
import google.appengine
|
||||
import google.appengine # noqa: unused import
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
server_software = os.environ.get(_SERVER_SOFTWARE, '')
|
||||
if server_software.startswith('Google App Engine/'):
|
||||
SETTINGS.env_name = 'GAE_PRODUCTION'
|
||||
@ -1106,8 +1142,6 @@ def _in_gae_environment():
|
||||
elif server_software.startswith('Development/'):
|
||||
SETTINGS.env_name = 'GAE_LOCAL'
|
||||
return True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
@ -1152,6 +1186,11 @@ class GoogleCredentials(OAuth2Credentials):
|
||||
print(response)
|
||||
"""
|
||||
|
||||
NON_SERIALIZED_MEMBERS = (
|
||||
frozenset(['_private_key']) |
|
||||
OAuth2Credentials.NON_SERIALIZED_MEMBERS)
|
||||
"""Members that aren't serialized when object is converted to JSON."""
|
||||
|
||||
def __init__(self, access_token, client_id, client_secret, refresh_token,
|
||||
token_expiry, token_uri, user_agent,
|
||||
revoke_uri=GOOGLE_REVOKE_URI):
|
||||
@ -1194,6 +1233,32 @@ class GoogleCredentials(OAuth2Credentials):
|
||||
"""
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, json_data):
|
||||
# TODO(issue 388): eliminate the circularity that is the reason for
|
||||
# this non-top-level import.
|
||||
from oauth2client.service_account import ServiceAccountCredentials
|
||||
data = json.loads(_from_bytes(json_data))
|
||||
|
||||
# We handle service_account.ServiceAccountCredentials since it is a
|
||||
# possible return type of GoogleCredentials.get_application_default()
|
||||
if (data['_module'] == 'oauth2client.service_account' and
|
||||
data['_class'] == 'ServiceAccountCredentials'):
|
||||
return ServiceAccountCredentials.from_json(data)
|
||||
|
||||
token_expiry = _parse_expiry(data.get('token_expiry'))
|
||||
google_credentials = cls(
|
||||
data['access_token'],
|
||||
data['client_id'],
|
||||
data['client_secret'],
|
||||
data['refresh_token'],
|
||||
token_expiry,
|
||||
data['token_uri'],
|
||||
data['user_agent'],
|
||||
revoke_uri=data.get('revoke_uri', None))
|
||||
google_credentials.invalid = data['invalid']
|
||||
return google_credentials
|
||||
|
||||
@property
|
||||
def serialization_data(self):
|
||||
"""Get the fields and values identifying the current credentials."""
|
||||
@ -1407,9 +1472,6 @@ def _get_well_known_file():
|
||||
"""Get the well known file produced by command 'gcloud auth login'."""
|
||||
# TODO(orestica): Revisit this method once gcloud provides a better way
|
||||
# of pinpointing the exact location of the file.
|
||||
|
||||
WELL_KNOWN_CREDENTIALS_FILE = 'application_default_credentials.json'
|
||||
|
||||
default_config_dir = os.getenv(_CLOUDSDK_CONFIG_ENV_VAR)
|
||||
if default_config_dir is None:
|
||||
if os.name == 'nt':
|
||||
@ -1427,14 +1489,11 @@ def _get_well_known_file():
|
||||
'.config',
|
||||
_CLOUDSDK_CONFIG_DIRECTORY)
|
||||
|
||||
return os.path.join(default_config_dir, WELL_KNOWN_CREDENTIALS_FILE)
|
||||
return os.path.join(default_config_dir, _WELL_KNOWN_CREDENTIALS_FILE)
|
||||
|
||||
|
||||
def _get_application_default_credential_from_file(filename):
|
||||
"""Build the Application Default Credentials from file."""
|
||||
|
||||
from oauth2client import service_account
|
||||
|
||||
# read the credentials from the file
|
||||
with open(filename) as file_obj:
|
||||
client_credentials = json.load(file_obj)
|
||||
@ -1465,12 +1524,9 @@ def _get_application_default_credential_from_file(filename):
|
||||
token_uri=GOOGLE_TOKEN_URI,
|
||||
user_agent='Python client library')
|
||||
else: # client_credentials['type'] == SERVICE_ACCOUNT
|
||||
return service_account._ServiceAccountCredentials(
|
||||
service_account_id=client_credentials['client_id'],
|
||||
service_account_email=client_credentials['client_email'],
|
||||
private_key_id=client_credentials['private_key_id'],
|
||||
private_key_pkcs8_text=client_credentials['private_key'],
|
||||
scopes=[])
|
||||
from oauth2client.service_account import ServiceAccountCredentials
|
||||
return ServiceAccountCredentials.from_json_keyfile_dict(
|
||||
client_credentials)
|
||||
|
||||
|
||||
def _raise_exception_for_missing_fields(missing_fields):
|
||||
@ -1487,15 +1543,15 @@ def _raise_exception_for_reading_json(credential_file,
|
||||
|
||||
|
||||
def _get_application_default_credential_GAE():
|
||||
from oauth2client.appengine import AppAssertionCredentials
|
||||
from oauth2client.contrib.appengine import AppAssertionCredentials
|
||||
|
||||
return AppAssertionCredentials([])
|
||||
|
||||
|
||||
def _get_application_default_credential_GCE():
|
||||
from oauth2client.gce import AppAssertionCredentials
|
||||
from oauth2client.contrib.gce import AppAssertionCredentials
|
||||
|
||||
return AppAssertionCredentials([])
|
||||
return AppAssertionCredentials()
|
||||
|
||||
|
||||
class AssertionCredentials(GoogleCredentials):
|
||||
@ -1561,6 +1617,18 @@ class AssertionCredentials(GoogleCredentials):
|
||||
"""
|
||||
self._do_revoke(http_request, self.access_token)
|
||||
|
||||
def sign_blob(self, blob):
|
||||
"""Cryptographically sign a blob (of bytes).
|
||||
|
||||
Args:
|
||||
blob: bytes, Message to be signed.
|
||||
|
||||
Returns:
|
||||
tuple, A pair of the private key ID used to sign the blob and
|
||||
the signed contents.
|
||||
"""
|
||||
raise NotImplementedError('This method is abstract.')
|
||||
|
||||
|
||||
def _RequireCryptoOrDie():
|
||||
"""Ensure we have a crypto library, or throw CryptoUnavailableError.
|
||||
@ -1573,102 +1641,6 @@ def _RequireCryptoOrDie():
|
||||
raise CryptoUnavailableError('No crypto library available')
|
||||
|
||||
|
||||
class SignedJwtAssertionCredentials(AssertionCredentials):
|
||||
"""Credentials object used for OAuth 2.0 Signed JWT assertion grants.
|
||||
|
||||
This credential does not require a flow to instantiate because it
|
||||
represents a two legged flow, and therefore has all of the required
|
||||
information to generate and refresh its own access tokens.
|
||||
|
||||
SignedJwtAssertionCredentials requires either PyOpenSSL, or PyCrypto
|
||||
2.6 or later. For App Engine you may also consider using
|
||||
AppAssertionCredentials.
|
||||
"""
|
||||
|
||||
MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
|
||||
|
||||
@util.positional(4)
|
||||
def __init__(self,
|
||||
service_account_name,
|
||||
private_key,
|
||||
scope,
|
||||
private_key_password='notasecret',
|
||||
user_agent=None,
|
||||
token_uri=GOOGLE_TOKEN_URI,
|
||||
revoke_uri=GOOGLE_REVOKE_URI,
|
||||
**kwargs):
|
||||
"""Constructor for SignedJwtAssertionCredentials.
|
||||
|
||||
Args:
|
||||
service_account_name: string, id for account, usually an email
|
||||
address.
|
||||
private_key: string, private key in PKCS12 or PEM format.
|
||||
scope: string or iterable of strings, scope(s) of the credentials
|
||||
being requested.
|
||||
private_key_password: string, password for private_key, unused if
|
||||
private_key is in PEM format.
|
||||
user_agent: string, HTTP User-Agent to provide for this
|
||||
application.
|
||||
token_uri: string, URI for token endpoint. For convenience defaults
|
||||
to Google's endpoints but any OAuth 2.0 provider can be
|
||||
used.
|
||||
revoke_uri: string, URI for revoke endpoint.
|
||||
kwargs: kwargs, Additional parameters to add to the JWT token, for
|
||||
example sub=joe@xample.org.
|
||||
|
||||
Raises:
|
||||
CryptoUnavailableError if no crypto library is available.
|
||||
"""
|
||||
_RequireCryptoOrDie()
|
||||
super(SignedJwtAssertionCredentials, self).__init__(
|
||||
None,
|
||||
user_agent=user_agent,
|
||||
token_uri=token_uri,
|
||||
revoke_uri=revoke_uri,
|
||||
)
|
||||
|
||||
self.scope = util.scopes_to_string(scope)
|
||||
|
||||
# Keep base64 encoded so it can be stored in JSON.
|
||||
self.private_key = base64.b64encode(private_key)
|
||||
self.private_key = _to_bytes(self.private_key, encoding='utf-8')
|
||||
self.private_key_password = private_key_password
|
||||
self.service_account_name = service_account_name
|
||||
self.kwargs = kwargs
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, s):
|
||||
data = json.loads(_from_bytes(s))
|
||||
retval = SignedJwtAssertionCredentials(
|
||||
data['service_account_name'],
|
||||
base64.b64decode(data['private_key']),
|
||||
data['scope'],
|
||||
private_key_password=data['private_key_password'],
|
||||
user_agent=data['user_agent'],
|
||||
token_uri=data['token_uri'],
|
||||
**data['kwargs']
|
||||
)
|
||||
retval.invalid = data['invalid']
|
||||
retval.access_token = data['access_token']
|
||||
return retval
|
||||
|
||||
def _generate_assertion(self):
|
||||
"""Generate the assertion that will be used in the request."""
|
||||
now = int(time.time())
|
||||
payload = {
|
||||
'aud': self.token_uri,
|
||||
'scope': self.scope,
|
||||
'iat': now,
|
||||
'exp': now + SignedJwtAssertionCredentials.MAX_TOKEN_LIFETIME_SECS,
|
||||
'iss': self.service_account_name
|
||||
}
|
||||
payload.update(self.kwargs)
|
||||
logger.debug(str(payload))
|
||||
|
||||
private_key = base64.b64decode(self.private_key)
|
||||
return crypt.make_signed_jwt(crypt.Signer.from_string(
|
||||
private_key, self.private_key_password), payload)
|
||||
|
||||
# Only used in verify_id_token(), which is always calling to the same URI
|
||||
# for the certs.
|
||||
_cached_http = httplib2.Http(MemoryCache())
|
||||
@ -1702,7 +1674,7 @@ def verify_id_token(id_token, audience, http=None,
|
||||
http = _cached_http
|
||||
|
||||
resp, content = http.request(cert_uri)
|
||||
if resp.status == 200:
|
||||
if resp.status == http_client.OK:
|
||||
certs = json.loads(_from_bytes(content))
|
||||
return crypt.verify_signed_jwt_with_certs(id_token, certs, audience)
|
||||
else:
|
||||
@ -2047,7 +2019,7 @@ class OAuth2WebServerFlow(Flow):
|
||||
resp, content = http.request(self.device_uri, method='POST', body=body,
|
||||
headers=headers)
|
||||
content = _from_bytes(content)
|
||||
if resp.status == 200:
|
||||
if resp.status == http_client.OK:
|
||||
try:
|
||||
flow_info = json.loads(content)
|
||||
except ValueError as e:
|
||||
@ -2130,7 +2102,7 @@ class OAuth2WebServerFlow(Flow):
|
||||
resp, content = http.request(self.token_uri, method='POST', body=body,
|
||||
headers=headers)
|
||||
d = _parse_exchange_token_response(content)
|
||||
if resp.status == 200 and 'access_token' in d:
|
||||
if resp.status == http_client.OK and 'access_token' in d:
|
||||
access_token = d['access_token']
|
||||
refresh_token = d.get('refresh_token', None)
|
||||
if not refresh_token:
|
||||
@ -2139,9 +2111,8 @@ class OAuth2WebServerFlow(Flow):
|
||||
"reauthenticating with approval_prompt='force'.")
|
||||
token_expiry = None
|
||||
if 'expires_in' in d:
|
||||
token_expiry = (
|
||||
datetime.datetime.utcnow() +
|
||||
datetime.timedelta(seconds=int(d['expires_in'])))
|
||||
delta = datetime.timedelta(seconds=int(d['expires_in']))
|
||||
token_expiry = delta + _UTCNOW()
|
||||
|
||||
extracted_id_token = None
|
||||
if 'id_token' in d:
|
||||
|
@ -121,8 +121,9 @@ def _loadfile(filename):
|
||||
try:
|
||||
with open(filename, 'r') as fp:
|
||||
obj = json.load(fp)
|
||||
except IOError:
|
||||
raise InvalidClientSecretsError('File not found: "%s"' % filename)
|
||||
except IOError as exc:
|
||||
raise InvalidClientSecretsError('Error opening file', exc.filename,
|
||||
exc.strerror, exc.errno)
|
||||
return _validate_clientsecrets(obj)
|
||||
|
||||
|
||||
|
6
src/oauth2client/contrib/__init__.py
Normal file
6
src/oauth2client/contrib/__init__.py
Normal file
@ -0,0 +1,6 @@
|
||||
"""Contributed modules.
|
||||
|
||||
Contrib contains modules that are not considered part of the core oauth2client
|
||||
library but provide additional functionality. These modules are intended to
|
||||
make it easier to use oauth2client.
|
||||
"""
|
163
src/oauth2client/contrib/_appengine_ndb.py
Normal file
163
src/oauth2client/contrib/_appengine_ndb.py
Normal file
@ -0,0 +1,163 @@
|
||||
# Copyright 2016 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Google App Engine utilities helper.
|
||||
|
||||
Classes that directly require App Engine's ndb library. Provided
|
||||
as a separate module in case of failure to import ndb while
|
||||
other App Engine libraries are present.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from google.appengine.ext import ndb
|
||||
|
||||
from oauth2client import client
|
||||
|
||||
|
||||
NDB_KEY = ndb.Key
|
||||
"""Key constant used by :mod:`oauth2client.contrib.appengine`."""
|
||||
|
||||
NDB_MODEL = ndb.Model
|
||||
"""Model constant used by :mod:`oauth2client.contrib.appengine`."""
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SiteXsrfSecretKeyNDB(ndb.Model):
|
||||
"""NDB Model for storage for the sites XSRF secret key.
|
||||
|
||||
Since this model uses the same kind as SiteXsrfSecretKey, it can be
|
||||
used interchangeably. This simply provides an NDB model for interacting
|
||||
with the same data the DB model interacts with.
|
||||
|
||||
There should only be one instance stored of this model, the one used
|
||||
for the site.
|
||||
"""
|
||||
secret = ndb.StringProperty()
|
||||
|
||||
@classmethod
|
||||
def _get_kind(cls):
|
||||
"""Return the kind name for this class."""
|
||||
return 'SiteXsrfSecretKey'
|
||||
|
||||
|
||||
class FlowNDBProperty(ndb.PickleProperty):
|
||||
"""App Engine NDB datastore Property for Flow.
|
||||
|
||||
Serves the same purpose as the DB FlowProperty, but for NDB models.
|
||||
Since PickleProperty inherits from BlobProperty, the underlying
|
||||
representation of the data in the datastore will be the same as in the
|
||||
DB case.
|
||||
|
||||
Utility property that allows easy storage and retrieval of an
|
||||
oauth2client.Flow
|
||||
"""
|
||||
|
||||
def _validate(self, value):
|
||||
"""Validates a value as a proper Flow object.
|
||||
|
||||
Args:
|
||||
value: A value to be set on the property.
|
||||
|
||||
Raises:
|
||||
TypeError if the value is not an instance of Flow.
|
||||
"""
|
||||
_LOGGER.info('validate: Got type %s', type(value))
|
||||
if value is not None and not isinstance(value, client.Flow):
|
||||
raise TypeError('Property %s must be convertible to a flow '
|
||||
'instance; received: %s.' % (self._name,
|
||||
value))
|
||||
|
||||
|
||||
class CredentialsNDBProperty(ndb.BlobProperty):
|
||||
"""App Engine NDB datastore Property for Credentials.
|
||||
|
||||
Serves the same purpose as the DB CredentialsProperty, but for NDB
|
||||
models. Since CredentialsProperty stores data as a blob and this
|
||||
inherits from BlobProperty, the data in the datastore will be the same
|
||||
as in the DB case.
|
||||
|
||||
Utility property that allows easy storage and retrieval of Credentials
|
||||
and subclasses.
|
||||
"""
|
||||
|
||||
def _validate(self, value):
|
||||
"""Validates a value as a proper credentials object.
|
||||
|
||||
Args:
|
||||
value: A value to be set on the property.
|
||||
|
||||
Raises:
|
||||
TypeError if the value is not an instance of Credentials.
|
||||
"""
|
||||
_LOGGER.info('validate: Got type %s', type(value))
|
||||
if value is not None and not isinstance(value, client.Credentials):
|
||||
raise TypeError('Property %s must be convertible to a '
|
||||
'credentials instance; received: %s.' %
|
||||
(self._name, value))
|
||||
|
||||
def _to_base_type(self, value):
|
||||
"""Converts our validated value to a JSON serialized string.
|
||||
|
||||
Args:
|
||||
value: A value to be set in the datastore.
|
||||
|
||||
Returns:
|
||||
A JSON serialized version of the credential, else '' if value
|
||||
is None.
|
||||
"""
|
||||
if value is None:
|
||||
return ''
|
||||
else:
|
||||
return value.to_json()
|
||||
|
||||
def _from_base_type(self, value):
|
||||
"""Converts our stored JSON string back to the desired type.
|
||||
|
||||
Args:
|
||||
value: A value from the datastore to be converted to the
|
||||
desired type.
|
||||
|
||||
Returns:
|
||||
A deserialized Credentials (or subclass) object, else None if
|
||||
the value can't be parsed.
|
||||
"""
|
||||
if not value:
|
||||
return None
|
||||
try:
|
||||
# Uses the from_json method of the implied class of value
|
||||
credentials = client.Credentials.new_from_json(value)
|
||||
except ValueError:
|
||||
credentials = None
|
||||
return credentials
|
||||
|
||||
|
||||
class CredentialsNDBModel(ndb.Model):
|
||||
"""NDB Model for storage of OAuth 2.0 Credentials
|
||||
|
||||
Since this model uses the same kind as CredentialsModel and has a
|
||||
property which can serialize and deserialize Credentials correctly, it
|
||||
can be used interchangeably with a CredentialsModel to access, insert
|
||||
and delete the same entities. This simply provides an NDB model for
|
||||
interacting with the same data the DB model interacts with.
|
||||
|
||||
Storage of the model is keyed by the user.user_id().
|
||||
"""
|
||||
credentials = CredentialsNDBProperty()
|
||||
|
||||
@classmethod
|
||||
def _get_kind(cls):
|
||||
"""Return the kind name for this class."""
|
||||
return 'CredentialsModel'
|
@ -32,26 +32,25 @@ from google.appengine.api import memcache
|
||||
from google.appengine.api import users
|
||||
from google.appengine.ext import db
|
||||
from google.appengine.ext.webapp.util import login_required
|
||||
from google.appengine.ext.webapp.util import run_wsgi_app
|
||||
|
||||
from oauth2client import GOOGLE_AUTH_URI
|
||||
from oauth2client import GOOGLE_REVOKE_URI
|
||||
from oauth2client import GOOGLE_TOKEN_URI
|
||||
from oauth2client import clientsecrets
|
||||
from oauth2client import util
|
||||
from oauth2client import xsrfutil
|
||||
from oauth2client.client import AccessTokenRefreshError
|
||||
from oauth2client.client import AssertionCredentials
|
||||
from oauth2client.client import Credentials
|
||||
from oauth2client.client import Flow
|
||||
from oauth2client.client import OAuth2WebServerFlow
|
||||
from oauth2client.client import Storage
|
||||
from oauth2client.contrib import xsrfutil
|
||||
|
||||
# TODO(dhermes): Resolve import issue.
|
||||
# This is a temporary fix for a Google internal issue.
|
||||
try:
|
||||
from google.appengine.ext import ndb
|
||||
except ImportError:
|
||||
ndb = None
|
||||
from oauth2client.contrib import _appengine_ndb
|
||||
except ImportError: # pragma: NO COVER
|
||||
_appengine_ndb = None
|
||||
|
||||
|
||||
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
|
||||
@ -62,6 +61,21 @@ OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns'
|
||||
|
||||
XSRF_MEMCACHE_ID = 'xsrf_secret_key'
|
||||
|
||||
if _appengine_ndb is None:
|
||||
CredentialsNDBModel = None
|
||||
CredentialsNDBProperty = None
|
||||
FlowNDBProperty = None
|
||||
_NDB_KEY = None
|
||||
_NDB_MODEL = None
|
||||
SiteXsrfSecretKeyNDB = None
|
||||
else:
|
||||
CredentialsNDBModel = _appengine_ndb.CredentialsNDBModel
|
||||
CredentialsNDBProperty = _appengine_ndb.CredentialsNDBProperty
|
||||
FlowNDBProperty = _appengine_ndb.FlowNDBProperty
|
||||
_NDB_KEY = _appengine_ndb.NDB_KEY
|
||||
_NDB_MODEL = _appengine_ndb.NDB_MODEL
|
||||
SiteXsrfSecretKeyNDB = _appengine_ndb.SiteXsrfSecretKeyNDB
|
||||
|
||||
|
||||
def _safe_html(s):
|
||||
"""Escape text to make it safe to display.
|
||||
@ -91,24 +105,6 @@ class SiteXsrfSecretKey(db.Model):
|
||||
"""
|
||||
secret = db.StringProperty()
|
||||
|
||||
if ndb is not None:
|
||||
class SiteXsrfSecretKeyNDB(ndb.Model):
|
||||
"""NDB Model for storage for the sites XSRF secret key.
|
||||
|
||||
Since this model uses the same kind as SiteXsrfSecretKey, it can be
|
||||
used interchangeably. This simply provides an NDB model for interacting
|
||||
with the same data the DB model interacts with.
|
||||
|
||||
There should only be one instance stored of this model, the one used
|
||||
for the site.
|
||||
"""
|
||||
secret = ndb.StringProperty()
|
||||
|
||||
@classmethod
|
||||
def _get_kind(cls):
|
||||
"""Return the kind name for this class."""
|
||||
return 'SiteXsrfSecretKey'
|
||||
|
||||
|
||||
def _generate_new_xsrf_secret_key():
|
||||
"""Returns a random XSRF secret key."""
|
||||
@ -166,6 +162,7 @@ class AppAssertionCredentials(AssertionCredentials):
|
||||
self.scope = util.scopes_to_string(scope)
|
||||
self._kwargs = kwargs
|
||||
self.service_account_id = kwargs.get('service_account_id', None)
|
||||
self._service_account_email = None
|
||||
|
||||
# Assertion type is no longer used, but still in the
|
||||
# parent class signature.
|
||||
@ -210,6 +207,34 @@ class AppAssertionCredentials(AssertionCredentials):
|
||||
def create_scoped(self, scopes):
|
||||
return AppAssertionCredentials(scopes, **self._kwargs)
|
||||
|
||||
def sign_blob(self, blob):
|
||||
"""Cryptographically sign a blob (of bytes).
|
||||
|
||||
Implements abstract method
|
||||
:meth:`oauth2client.client.AssertionCredentials.sign_blob`.
|
||||
|
||||
Args:
|
||||
blob: bytes, Message to be signed.
|
||||
|
||||
Returns:
|
||||
tuple, A pair of the private key ID used to sign the blob and
|
||||
the signed contents.
|
||||
"""
|
||||
return app_identity.sign_blob(blob)
|
||||
|
||||
@property
|
||||
def service_account_email(self):
|
||||
"""Get the email for the current service account.
|
||||
|
||||
Returns:
|
||||
string, The email associated with the Google App Engine
|
||||
service account.
|
||||
"""
|
||||
if self._service_account_email is None:
|
||||
self._service_account_email = (
|
||||
app_identity.get_service_account_name())
|
||||
return self._service_account_email
|
||||
|
||||
|
||||
class FlowProperty(db.Property):
|
||||
"""App Engine datastore Property for Flow.
|
||||
@ -244,35 +269,6 @@ class FlowProperty(db.Property):
|
||||
return not value
|
||||
|
||||
|
||||
if ndb is not None:
|
||||
class FlowNDBProperty(ndb.PickleProperty):
|
||||
"""App Engine NDB datastore Property for Flow.
|
||||
|
||||
Serves the same purpose as the DB FlowProperty, but for NDB models.
|
||||
Since PickleProperty inherits from BlobProperty, the underlying
|
||||
representation of the data in the datastore will be the same as in the
|
||||
DB case.
|
||||
|
||||
Utility property that allows easy storage and retrieval of an
|
||||
oauth2client.Flow
|
||||
"""
|
||||
|
||||
def _validate(self, value):
|
||||
"""Validates a value as a proper Flow object.
|
||||
|
||||
Args:
|
||||
value: A value to be set on the property.
|
||||
|
||||
Raises:
|
||||
TypeError if the value is not an instance of Flow.
|
||||
"""
|
||||
logger.info('validate: Got type %s', type(value))
|
||||
if value is not None and not isinstance(value, Flow):
|
||||
raise TypeError('Property %s must be convertible to a flow '
|
||||
'instance; received: %s.' % (self._name,
|
||||
value))
|
||||
|
||||
|
||||
class CredentialsProperty(db.Property):
|
||||
"""App Engine datastore Property for Credentials.
|
||||
|
||||
@ -317,73 +313,6 @@ class CredentialsProperty(db.Property):
|
||||
return value
|
||||
|
||||
|
||||
if ndb is not None:
|
||||
# TODO(dhermes): Turn this into a JsonProperty and overhaul the Credentials
|
||||
# and subclass mechanics to use new_from_dict, to_dict,
|
||||
# from_dict, etc.
|
||||
class CredentialsNDBProperty(ndb.BlobProperty):
|
||||
"""App Engine NDB datastore Property for Credentials.
|
||||
|
||||
Serves the same purpose as the DB CredentialsProperty, but for NDB
|
||||
models. Since CredentialsProperty stores data as a blob and this
|
||||
inherits from BlobProperty, the data in the datastore will be the same
|
||||
as in the DB case.
|
||||
|
||||
Utility property that allows easy storage and retrieval of Credentials
|
||||
and subclasses.
|
||||
"""
|
||||
|
||||
def _validate(self, value):
|
||||
"""Validates a value as a proper credentials object.
|
||||
|
||||
Args:
|
||||
value: A value to be set on the property.
|
||||
|
||||
Raises:
|
||||
TypeError if the value is not an instance of Credentials.
|
||||
"""
|
||||
logger.info('validate: Got type %s', type(value))
|
||||
if value is not None and not isinstance(value, Credentials):
|
||||
raise TypeError('Property %s must be convertible to a '
|
||||
'credentials instance; received: %s.' %
|
||||
(self._name, value))
|
||||
|
||||
def _to_base_type(self, value):
|
||||
"""Converts our validated value to a JSON serialized string.
|
||||
|
||||
Args:
|
||||
value: A value to be set in the datastore.
|
||||
|
||||
Returns:
|
||||
A JSON serialized version of the credential, else '' if value
|
||||
is None.
|
||||
"""
|
||||
if value is None:
|
||||
return ''
|
||||
else:
|
||||
return value.to_json()
|
||||
|
||||
def _from_base_type(self, value):
|
||||
"""Converts our stored JSON string back to the desired type.
|
||||
|
||||
Args:
|
||||
value: A value from the datastore to be converted to the
|
||||
desired type.
|
||||
|
||||
Returns:
|
||||
A deserialized Credentials (or subclass) object, else None if
|
||||
the value can't be parsed.
|
||||
"""
|
||||
if not value:
|
||||
return None
|
||||
try:
|
||||
# Uses the from_json method of the implied class of value
|
||||
credentials = Credentials.new_from_json(value)
|
||||
except ValueError:
|
||||
credentials = None
|
||||
return credentials
|
||||
|
||||
|
||||
class StorageByKeyName(Storage):
|
||||
"""Store and retrieve a credential to and from the App Engine datastore.
|
||||
|
||||
@ -408,6 +337,8 @@ class StorageByKeyName(Storage):
|
||||
user: users.User object, optional. Can be used to grab user ID as a
|
||||
key_name if no key name is specified.
|
||||
"""
|
||||
super(StorageByKeyName, self).__init__()
|
||||
|
||||
if key_name is None:
|
||||
if user is None:
|
||||
raise ValueError('StorageByKeyName called with no '
|
||||
@ -429,7 +360,7 @@ class StorageByKeyName(Storage):
|
||||
# need worry about new-style classes since ndb and db models are
|
||||
# new-style
|
||||
if isinstance(self._model, type):
|
||||
if ndb is not None and issubclass(self._model, ndb.Model):
|
||||
if _NDB_MODEL is not None and issubclass(self._model, _NDB_MODEL):
|
||||
return True
|
||||
elif issubclass(self._model, db.Model):
|
||||
return False
|
||||
@ -458,7 +389,7 @@ class StorageByKeyName(Storage):
|
||||
not the given key is in the datastore.
|
||||
"""
|
||||
if self._is_ndb():
|
||||
ndb.Key(self._model, self._key_name).delete()
|
||||
_NDB_KEY(self._model, self._key_name).delete()
|
||||
else:
|
||||
entity_key = db.Key.from_path(self._model.kind(), self._key_name)
|
||||
db.delete(entity_key)
|
||||
@ -517,26 +448,6 @@ class CredentialsModel(db.Model):
|
||||
credentials = CredentialsProperty()
|
||||
|
||||
|
||||
if ndb is not None:
|
||||
class CredentialsNDBModel(ndb.Model):
|
||||
"""NDB Model for storage of OAuth 2.0 Credentials
|
||||
|
||||
Since this model uses the same kind as CredentialsModel and has a
|
||||
property which can serialize and deserialize Credentials correctly, it
|
||||
can be used interchangeably with a CredentialsModel to access, insert
|
||||
and delete the same entities. This simply provides an NDB model for
|
||||
interacting with the same data the DB model interacts with.
|
||||
|
||||
Storage of the model is keyed by the user.user_id().
|
||||
"""
|
||||
credentials = CredentialsNDBProperty()
|
||||
|
||||
@classmethod
|
||||
def _get_kind(cls):
|
||||
"""Return the kind name for this class."""
|
||||
return 'CredentialsModel'
|
||||
|
||||
|
||||
def _build_state_value(request_handler, user):
|
||||
"""Composes the value for the 'state' parameter.
|
||||
|
@ -14,6 +14,7 @@
|
||||
|
||||
"""OAuth 2.0 utitilies for Google Developer Shell environment."""
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
@ -21,6 +22,10 @@ import socket
|
||||
from oauth2client._helpers import _to_bytes
|
||||
from oauth2client import client
|
||||
|
||||
# Expose utcnow() at module level to allow for
|
||||
# easier testing (by replacing with a stub).
|
||||
_UTCNOW = datetime.datetime.utcnow
|
||||
|
||||
DEVSHELL_ENV = 'DEVSHELL_CLIENT_PORT'
|
||||
|
||||
|
||||
@ -52,6 +57,7 @@ class CredentialInfoResponse(object):
|
||||
* Index 0 - user email
|
||||
* Index 1 - default project ID. None if the project context is not known.
|
||||
* Index 2 - OAuth2 access token. None if there is no valid auth context.
|
||||
* Index 3 - Seconds until the access token expires. None if not present.
|
||||
"""
|
||||
|
||||
def __init__(self, json_string):
|
||||
@ -63,6 +69,7 @@ class CredentialInfoResponse(object):
|
||||
self.user_email = pbl[0] if pbl_len > 0 else None
|
||||
self.project_id = pbl[1] if pbl_len > 1 else None
|
||||
self.access_token = pbl[2] if pbl_len > 2 else None
|
||||
self.expires_in = pbl[3] if pbl_len > 3 else None
|
||||
|
||||
|
||||
def _SendRecv():
|
||||
@ -117,6 +124,12 @@ class DevshellCredentials(client.GoogleCredentials):
|
||||
def _refresh(self, http_request):
|
||||
self.devshell_response = _SendRecv()
|
||||
self.access_token = self.devshell_response.access_token
|
||||
expires_in = self.devshell_response.expires_in
|
||||
if expires_in is not None:
|
||||
delta = datetime.timedelta(seconds=expires_in)
|
||||
self.token_expiry = _UTCNOW() + delta
|
||||
else:
|
||||
self.token_expiry = None
|
||||
|
||||
@property
|
||||
def user_email(self):
|
66
src/oauth2client/contrib/dictionary_storage.py
Normal file
66
src/oauth2client/contrib/dictionary_storage.py
Normal file
@ -0,0 +1,66 @@
|
||||
# Copyright 2016 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Dictionary storage for OAuth2 Credentials."""
|
||||
|
||||
from oauth2client.client import OAuth2Credentials
|
||||
from oauth2client.client import Storage
|
||||
|
||||
|
||||
class DictionaryStorage(Storage):
|
||||
"""Store and retrieve credentials to and from a dictionary-like object.
|
||||
|
||||
Args:
|
||||
dictionary: A dictionary or dictionary-like object.
|
||||
key: A string or other hashable. The credentials will be stored in
|
||||
``dictionary[key]``.
|
||||
lock: An optional threading.Lock-like object. The lock will be
|
||||
acquired before anything is written or read from the
|
||||
dictionary.
|
||||
"""
|
||||
|
||||
def __init__(self, dictionary, key, lock=None):
|
||||
"""Construct a DictionaryStorage instance."""
|
||||
super(DictionaryStorage, self).__init__(lock=lock)
|
||||
self._dictionary = dictionary
|
||||
self._key = key
|
||||
|
||||
def locked_get(self):
|
||||
"""Retrieve the credentials from the dictionary, if they exist.
|
||||
|
||||
Returns: A :class:`oauth2client.client.OAuth2Credentials` instance.
|
||||
"""
|
||||
serialized = self._dictionary.get(self._key)
|
||||
|
||||
if serialized is None:
|
||||
return None
|
||||
|
||||
credentials = OAuth2Credentials.from_json(serialized)
|
||||
credentials.set_store(self)
|
||||
|
||||
return credentials
|
||||
|
||||
def locked_put(self, credentials):
|
||||
"""Save the credentials to the dictionary.
|
||||
|
||||
Args:
|
||||
credentials: A :class:`oauth2client.client.OAuth2Credentials`
|
||||
instance.
|
||||
"""
|
||||
serialized = credentials.to_json()
|
||||
self._dictionary[self._key] = serialized
|
||||
|
||||
def locked_delete(self):
|
||||
"""Remove the credentials from the dictionary, if they exist."""
|
||||
self._dictionary.pop(self._key, None)
|
@ -21,17 +21,17 @@ the Django datastore.
|
||||
import oauth2client
|
||||
import base64
|
||||
import pickle
|
||||
import six
|
||||
|
||||
from django.db import models
|
||||
from django.utils.encoding import smart_bytes, smart_text
|
||||
from oauth2client.client import Storage as BaseStorage
|
||||
|
||||
|
||||
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
|
||||
|
||||
|
||||
class CredentialsField(models.Field):
|
||||
|
||||
__metaclass__ = models.SubfieldBase
|
||||
class CredentialsField(six.with_metaclass(models.SubfieldBase, models.Field)):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if 'null' not in kwargs:
|
||||
@ -39,24 +39,36 @@ class CredentialsField(models.Field):
|
||||
super(CredentialsField, self).__init__(*args, **kwargs)
|
||||
|
||||
def get_internal_type(self):
|
||||
return "TextField"
|
||||
return 'TextField'
|
||||
|
||||
def to_python(self, value):
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, oauth2client.client.Credentials):
|
||||
return value
|
||||
return pickle.loads(base64.b64decode(value))
|
||||
return pickle.loads(base64.b64decode(smart_bytes(value)))
|
||||
|
||||
def get_db_prep_value(self, value, connection, prepared=False):
|
||||
def get_prep_value(self, value):
|
||||
if value is None:
|
||||
return None
|
||||
return base64.b64encode(pickle.dumps(value))
|
||||
return smart_text(base64.b64encode(pickle.dumps(value)))
|
||||
|
||||
def value_to_string(self, obj):
|
||||
"""Convert the field value from the provided model to a string.
|
||||
|
||||
Used during model serialization.
|
||||
|
||||
Args:
|
||||
obj: db.Model, model object
|
||||
|
||||
Returns:
|
||||
string, the serialized field value
|
||||
"""
|
||||
value = self._get_val_from_obj(obj)
|
||||
return self.get_prep_value(value)
|
||||
|
||||
|
||||
class FlowField(models.Field):
|
||||
|
||||
__metaclass__ = models.SubfieldBase
|
||||
class FlowField(six.with_metaclass(models.SubfieldBase, models.Field)):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if 'null' not in kwargs:
|
||||
@ -64,7 +76,7 @@ class FlowField(models.Field):
|
||||
super(FlowField, self).__init__(*args, **kwargs)
|
||||
|
||||
def get_internal_type(self):
|
||||
return "TextField"
|
||||
return 'TextField'
|
||||
|
||||
def to_python(self, value):
|
||||
if value is None:
|
||||
@ -73,14 +85,28 @@ class FlowField(models.Field):
|
||||
return value
|
||||
return pickle.loads(base64.b64decode(value))
|
||||
|
||||
def get_db_prep_value(self, value, connection, prepared=False):
|
||||
def get_prep_value(self, value):
|
||||
if value is None:
|
||||
return None
|
||||
return base64.b64encode(pickle.dumps(value))
|
||||
return smart_text(base64.b64encode(pickle.dumps(value)))
|
||||
|
||||
def value_to_string(self, obj):
|
||||
"""Convert the field value from the provided model to a string.
|
||||
|
||||
Used during model serialization.
|
||||
|
||||
Args:
|
||||
obj: db.Model, model object
|
||||
|
||||
Returns:
|
||||
string, the serialized field value
|
||||
"""
|
||||
value = self._get_val_from_obj(obj)
|
||||
return self.get_prep_value(value)
|
||||
|
||||
|
||||
class Storage(BaseStorage):
|
||||
"""Store and retrieve a single credential to and from the datastore.
|
||||
"""Store and retrieve a single credential to and from the Django datastore.
|
||||
|
||||
This Storage helper presumes the Credentials
|
||||
have been stored as a CredenialsField
|
||||
@ -98,13 +124,14 @@ class Storage(BaseStorage):
|
||||
property_name: string, name of the property that is an
|
||||
CredentialsProperty
|
||||
"""
|
||||
super(Storage, self).__init__()
|
||||
self.model_class = model_class
|
||||
self.key_name = key_name
|
||||
self.key_value = key_value
|
||||
self.property_name = property_name
|
||||
|
||||
def locked_get(self):
|
||||
"""Retrieve Credential from datastore.
|
||||
"""Retrieve stored credential.
|
||||
|
||||
Returns:
|
||||
oauth2client.Credentials
|
||||
@ -120,7 +147,7 @@ class Storage(BaseStorage):
|
||||
return credential
|
||||
|
||||
def locked_put(self, credentials, overwrite=False):
|
||||
"""Write a Credentials to the datastore.
|
||||
"""Write a Credentials to the Django datastore.
|
||||
|
||||
Args:
|
||||
credentials: Credentials, the credentials to store.
|
306
src/oauth2client/contrib/django_util/__init__.py
Normal file
306
src/oauth2client/contrib/django_util/__init__.py
Normal file
@ -0,0 +1,306 @@
|
||||
# Copyright 2015 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Utilities for the Django web framework
|
||||
|
||||
Provides Django views and helpers the make using the OAuth2 web server
|
||||
flow easier. It includes an ``oauth_required`` decorator to automatically ensure
|
||||
that user credentials are available, and an ``oauth_enabled`` decorator to check
|
||||
if the user has authorized, and helper shortcuts to create the authorization
|
||||
URL otherwise.
|
||||
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
To configure, you'll need a set of OAuth2 web application credentials from
|
||||
`Google Developer's Console <https://console.developers.google.com/project/_/apiui/credential>`.
|
||||
|
||||
Add the helper to your INSTALLED_APPS:
|
||||
|
||||
.. code-block:: python
|
||||
:caption: settings.py
|
||||
:name: installed_apps
|
||||
|
||||
INSTALLED_APPS = (
|
||||
# other apps
|
||||
"oauth2client.contrib.django_util"
|
||||
)
|
||||
|
||||
Add the client secrets created earlier to the settings. You can either
|
||||
specify the path to the credentials file in JSON format
|
||||
|
||||
.. code-block:: python
|
||||
:caption: settings.py
|
||||
:name: secrets_file
|
||||
|
||||
GOOGLE_OAUTH2_CLIENT_SECRETS_JSON=/path/to/client-secret.json
|
||||
|
||||
Or, directly configure the client Id and client secret.
|
||||
|
||||
|
||||
.. code-block:: python
|
||||
:caption: settings.py
|
||||
:name: secrets_config
|
||||
|
||||
GOOGLE_OAUTH2_CLIENT_ID=client-id-field
|
||||
GOOGLE_OAUTH2_CLIENT_SECRET=client-secret-field
|
||||
|
||||
By default, the default scopes for the required decorator only contains the
|
||||
``email`` scopes. You can change that default in the settings.
|
||||
|
||||
.. code-block:: python
|
||||
:caption: settings.py
|
||||
:name: scopes
|
||||
|
||||
GOOGLE_OAUTH2_SCOPES = ('email', 'https://www.googleapis.com/auth/calendar',)
|
||||
|
||||
By default, the decorators will add an `oauth` object to the Django request
|
||||
object, and include all of its state and helpers inside that object. If the
|
||||
`oauth` name conflicts with another usage, it can be changed
|
||||
|
||||
.. code-block:: python
|
||||
:caption: settings.py
|
||||
:name: request_prefix
|
||||
|
||||
# changes request.oauth to request.google_oauth
|
||||
GOOGLE_OAUTH2_REQUEST_ATTRIBUTE = 'google_oauth'
|
||||
|
||||
Add the oauth2 routes to your application's urls.py urlpatterns.
|
||||
|
||||
.. code-block:: python
|
||||
:caption: urls.py
|
||||
:name: urls
|
||||
|
||||
from oauth2client.contrib.django_util.site import urls as oauth2_urls
|
||||
|
||||
urlpatterns += [url(r'^oauth2/', include(oauth2_urls))]
|
||||
|
||||
To require OAuth2 credentials for a view, use the `oauth2_required` decorator.
|
||||
This creates a credentials object with an id_token, and allows you to create an
|
||||
`http` object to build service clients with. These are all attached to the
|
||||
request.oauth
|
||||
|
||||
.. code-block:: python
|
||||
:caption: views.py
|
||||
:name: views_required
|
||||
|
||||
from oauth2client.contrib.django_util.decorators import oauth_required
|
||||
|
||||
@oauth_required
|
||||
def requires_default_scopes(request):
|
||||
email = request.oauth.credentials.id_token['email']
|
||||
service = build(serviceName='calendar', version='v3',
|
||||
http=request.oauth.http,
|
||||
developerKey=API_KEY)
|
||||
events = service.events().list(calendarId='primary').execute()['items']
|
||||
return HttpResponse("email: %s , calendar: %s" % (email, str(events)))
|
||||
|
||||
To make OAuth2 optional and provide an authorization link in your own views.
|
||||
|
||||
.. code-block:: python
|
||||
:caption: views.py
|
||||
:name: views_enabled2
|
||||
|
||||
from oauth2client.contrib.django_util.decorators import oauth_enabled
|
||||
|
||||
@oauth_enabled
|
||||
def optional_oauth2(request):
|
||||
if request.oauth.has_credentials():
|
||||
# this could be passed into a view
|
||||
# request.oauth.http is also initialized
|
||||
return HttpResponse("User email: %s"
|
||||
% request.oauth.credentials.id_token['email'])
|
||||
else:
|
||||
return HttpResponse('Here is an OAuth Authorize link:
|
||||
<a href="%s">Authorize</a>' % request.oauth.get_authorize_redirect())
|
||||
|
||||
If a view needs a scope not included in the default scopes specified in
|
||||
the settings, you can use [incremental auth](https://developers.google.com/identity/sign-in/web/incremental-auth)
|
||||
and specify additional scopes in the decorator arguments.
|
||||
|
||||
.. code-block:: python
|
||||
:caption: views.py
|
||||
:name: views_required_additional_scopes
|
||||
|
||||
@oauth_enabled(scopes=['https://www.googleapis.com/auth/drive'])
|
||||
def drive_required(request):
|
||||
if request.oauth.has_credentials():
|
||||
service = build(serviceName='drive', version='v2',
|
||||
http=request.oauth.http,
|
||||
developerKey=API_KEY)
|
||||
events = service.files().list().execute()['items']
|
||||
return HttpResponse(str(events))
|
||||
else:
|
||||
return HttpResponse('Here is an OAuth Authorize link:
|
||||
<a href="%s">Authorize</a>' % request.oauth.get_authorize_redirect())
|
||||
|
||||
|
||||
To provide a callback on authorization being completed, use the
|
||||
oauth2_authorized signal:
|
||||
|
||||
.. code-block:: python
|
||||
:caption: views.py
|
||||
:name: signals
|
||||
|
||||
from oauth2client.contrib.django_util.signals import oauth2_authorized
|
||||
|
||||
def test_callback(sender, request, credentials, **kwargs):
|
||||
print "Authorization Signal Received %s" % credentials.id_token['email']
|
||||
|
||||
oauth2_authorized.connect(test_callback)
|
||||
|
||||
"""
|
||||
|
||||
import django.conf
|
||||
from django.core import exceptions
|
||||
from django.core import urlresolvers
|
||||
import httplib2
|
||||
from oauth2client import clientsecrets
|
||||
from oauth2client.contrib.django_util import storage
|
||||
from six.moves.urllib import parse
|
||||
|
||||
GOOGLE_OAUTH2_DEFAULT_SCOPES = ('email',)
|
||||
GOOGLE_OAUTH2_REQUEST_ATTRIBUTE = 'oauth'
|
||||
|
||||
|
||||
def _load_client_secrets(filename):
|
||||
"""Loads client secrets from the given filename."""
|
||||
client_type, client_info = clientsecrets.loadfile(filename)
|
||||
|
||||
if client_type != clientsecrets.TYPE_WEB:
|
||||
raise ValueError(
|
||||
'The flow specified in {} is not supported, only the WEB flow '
|
||||
'type is supported.'.format(client_type))
|
||||
return client_info['client_id'], client_info['client_secret']
|
||||
|
||||
|
||||
def _get_oauth2_client_id_and_secret(settings_instance):
|
||||
"""Initializes client id and client secret based on the settings"""
|
||||
secret_json = getattr(django.conf.settings,
|
||||
'GOOGLE_OAUTH2_CLIENT_SECRETS_JSON', None)
|
||||
if secret_json is not None:
|
||||
return _load_client_secrets(secret_json)
|
||||
else:
|
||||
client_id = getattr(settings_instance, "GOOGLE_OAUTH2_CLIENT_ID",
|
||||
None)
|
||||
client_secret = getattr(settings_instance,
|
||||
"GOOGLE_OAUTH2_CLIENT_SECRET", None)
|
||||
if client_id is not None and client_secret is not None:
|
||||
return client_id, client_secret
|
||||
else:
|
||||
raise exceptions.ImproperlyConfigured(
|
||||
"Must specify either GOOGLE_OAUTH2_CLIENT_SECRETS_JSON, or "
|
||||
" both GOOGLE_OAUTH2_CLIENT_ID and GOOGLE_OAUTH2_CLIENT_SECRET "
|
||||
"in settings.py")
|
||||
|
||||
|
||||
class OAuth2Settings(object):
|
||||
"""Initializes Django OAuth2 Helper Settings
|
||||
|
||||
This class loads the OAuth2 Settings from the Django settings, and then
|
||||
provides those settings as attributes to the rest of the views and
|
||||
decorators in the module.
|
||||
|
||||
Attributes:
|
||||
scopes: A list of OAuth2 scopes that the decorators and views will use
|
||||
as defaults
|
||||
request_prefix: The name of the attribute that the decorators use to
|
||||
attach the UserOAuth2 object to the Django request object.
|
||||
client_id: The OAuth2 Client ID
|
||||
client_secret: The OAuth2 Client Secret
|
||||
"""
|
||||
|
||||
def __init__(self, settings_instance):
|
||||
self.scopes = getattr(settings_instance, 'GOOGLE_OAUTH2_SCOPES',
|
||||
GOOGLE_OAUTH2_DEFAULT_SCOPES)
|
||||
self.request_prefix = getattr(settings_instance,
|
||||
'GOOGLE_OAUTH2_REQUEST_ATTRIBUTE',
|
||||
GOOGLE_OAUTH2_REQUEST_ATTRIBUTE)
|
||||
self.client_id, self.client_secret = \
|
||||
_get_oauth2_client_id_and_secret(settings_instance)
|
||||
|
||||
if ('django.contrib.sessions.middleware.SessionMiddleware'
|
||||
not in settings_instance.MIDDLEWARE_CLASSES):
|
||||
raise exceptions.ImproperlyConfigured(
|
||||
"The Google OAuth2 Helper requires session middleware to "
|
||||
"be installed. Edit your MIDDLEWARE_CLASSES setting"
|
||||
" to include 'django.contrib.sessions.middleware."
|
||||
"SessionMiddleware'.")
|
||||
|
||||
|
||||
oauth2_settings = OAuth2Settings(django.conf.settings)
|
||||
|
||||
|
||||
def _redirect_with_params(url_name, *args, **kwargs):
|
||||
"""Helper method to create a redirect response that uses GET URL
|
||||
parameters."""
|
||||
|
||||
url = urlresolvers.reverse(url_name, args=args)
|
||||
params = parse.urlencode(kwargs, True)
|
||||
return "{0}?{1}".format(url, params)
|
||||
|
||||
|
||||
class UserOAuth2(object):
|
||||
"""Class to create oauth2 objects on Django request objects containing
|
||||
credentials and helper methods.
|
||||
"""
|
||||
|
||||
def __init__(self, request, scopes=None, return_url=None):
|
||||
"""Initialize the Oauth2 Object
|
||||
:param request: Django request object
|
||||
:param scopes: Scopes desired for this OAuth2 flow
|
||||
:param return_url: URL to return to after authorization is complete
|
||||
:return:
|
||||
"""
|
||||
self.request = request
|
||||
self.return_url = return_url or request.get_full_path()
|
||||
self.scopes = set(oauth2_settings.scopes)
|
||||
if scopes:
|
||||
self.scopes |= set(scopes)
|
||||
|
||||
# make sure previously requested custom scopes are maintained
|
||||
# in future authorizations
|
||||
credentials = storage.get_storage(self.request).get()
|
||||
if credentials:
|
||||
self.scopes |= credentials.scopes
|
||||
|
||||
def get_authorize_redirect(self):
|
||||
"""Creates a URl to start the OAuth2 authorization flow"""
|
||||
get_params = {
|
||||
'return_url': self.return_url,
|
||||
'scopes': self.scopes
|
||||
}
|
||||
|
||||
return _redirect_with_params('google_oauth:authorize',
|
||||
**get_params)
|
||||
|
||||
def has_credentials(self):
|
||||
"""Returns True if there are valid credentials for the current user
|
||||
and required scopes."""
|
||||
return (self.credentials and not self.credentials.invalid
|
||||
and self.credentials.has_scopes(self.scopes))
|
||||
|
||||
@property
|
||||
def credentials(self):
|
||||
"""Gets the authorized credentials for this flow, if they exist"""
|
||||
return storage.get_storage(self.request).get()
|
||||
|
||||
@property
|
||||
def http(self):
|
||||
"""Helper method to create an HTTP client authorized with OAuth2
|
||||
credentials"""
|
||||
if self.has_credentials():
|
||||
return self.credentials.authorize(httplib2.Http())
|
||||
return None
|
32
src/oauth2client/contrib/django_util/apps.py
Normal file
32
src/oauth2client/contrib/django_util/apps.py
Normal file
@ -0,0 +1,32 @@
|
||||
# Copyright 2015 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Application Config For Django OAuth2 Helper
|
||||
|
||||
Django 1.7+ provides an
|
||||
[applications](https://docs.djangoproject.com/en/1.8/ref/applications/)
|
||||
API so that Django projects can introspect on installed applications using a
|
||||
stable API. This module exists to follow that convention.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
# Django 1.7+ only supports Python 2.7+
|
||||
if sys.hexversion >= 0x02070000: # pragma: NO COVER
|
||||
from django.apps import AppConfig
|
||||
|
||||
class GoogleOAuth2HelperConfig(AppConfig):
|
||||
""" App Config for Django Helper"""
|
||||
name = 'oauth2client.django_util'
|
||||
verbose_name = "Google OAuth2 Django Helper"
|
117
src/oauth2client/contrib/django_util/decorators.py
Normal file
117
src/oauth2client/contrib/django_util/decorators.py
Normal file
@ -0,0 +1,117 @@
|
||||
# Copyright 2015 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from django import shortcuts
|
||||
from oauth2client.contrib import django_util
|
||||
from six import wraps
|
||||
|
||||
|
||||
def oauth_required(decorated_function=None, scopes=None, **decorator_kwargs):
|
||||
""" Decorator to require OAuth2 credentials for a view
|
||||
|
||||
|
||||
.. code-block:: python
|
||||
:caption: views.py
|
||||
:name: views_required_2
|
||||
|
||||
|
||||
from oauth2client.django_util.decorators import oauth_required
|
||||
|
||||
@oauth_required
|
||||
def requires_default_scopes(request):
|
||||
email = request.credentials.id_token['email']
|
||||
service = build(serviceName='calendar', version='v3',
|
||||
http=request.oauth.http,
|
||||
developerKey=API_KEY)
|
||||
events = service.events().list(
|
||||
calendarId='primary').execute()['items']
|
||||
return HttpResponse("email: %s , calendar: %s" % (email, str(events)))
|
||||
|
||||
:param decorated_function: View function to decorate, must have the Django
|
||||
request object as the first argument
|
||||
:param scopes: Scopes to require, will default
|
||||
:param decorator_kwargs: Can include ``return_url`` to specify the URL to
|
||||
return to after OAuth2 authorization is complete
|
||||
:return: An OAuth2 Authorize view if credentials are not found or if the
|
||||
credentials are missing the required scopes. Otherwise,
|
||||
the decorated view.
|
||||
"""
|
||||
|
||||
def curry_wrapper(wrapped_function):
|
||||
@wraps(wrapped_function)
|
||||
def required_wrapper(request, *args, **kwargs):
|
||||
return_url = decorator_kwargs.pop('return_url',
|
||||
request.get_full_path())
|
||||
user_oauth = django_util.UserOAuth2(request, scopes, return_url)
|
||||
if not user_oauth.has_credentials():
|
||||
return shortcuts.redirect(user_oauth.get_authorize_redirect())
|
||||
setattr(request, django_util.oauth2_settings.request_prefix,
|
||||
user_oauth)
|
||||
return wrapped_function(request, *args, **kwargs)
|
||||
|
||||
return required_wrapper
|
||||
|
||||
if decorated_function:
|
||||
return curry_wrapper(decorated_function)
|
||||
else:
|
||||
return curry_wrapper
|
||||
|
||||
|
||||
def oauth_enabled(decorated_function=None, scopes=None, **decorator_kwargs):
|
||||
""" Decorator to enable OAuth Credentials if authorized, and setup
|
||||
the oauth object on the request object to provide helper functions
|
||||
to start the flow otherwise.
|
||||
|
||||
.. code-block:: python
|
||||
:caption: views.py
|
||||
:name: views_enabled3
|
||||
|
||||
from oauth2client.django_util.decorators import oauth_enabled
|
||||
|
||||
@oauth_enabled
|
||||
def optional_oauth2(request):
|
||||
if request.oauth.has_credentials():
|
||||
# this could be passed into a view
|
||||
# request.oauth.http is also initialized
|
||||
return HttpResponse("User email: %s" %
|
||||
request.oauth.credentials.id_token['email'])
|
||||
else:
|
||||
return HttpResponse('Here is an OAuth Authorize link:
|
||||
<a href="%s">Authorize</a>' %
|
||||
request.oauth.get_authorize_redirect())
|
||||
|
||||
|
||||
:param decorated_function: View function to decorate
|
||||
:param scopes: Scopes to require, will default
|
||||
:param decorator_kwargs: Can include ``return_url`` to specify the URL to
|
||||
return to after OAuth2 authorization is complete
|
||||
:return: The decorated view function
|
||||
"""
|
||||
|
||||
def curry_wrapper(wrapped_function):
|
||||
@wraps(wrapped_function)
|
||||
def enabled_wrapper(request, *args, **kwargs):
|
||||
return_url = decorator_kwargs.pop('return_url',
|
||||
request.get_full_path())
|
||||
user_oauth = django_util.UserOAuth2(request, scopes, return_url)
|
||||
setattr(request, django_util.oauth2_settings.request_prefix,
|
||||
user_oauth)
|
||||
return wrapped_function(request, *args, **kwargs)
|
||||
|
||||
return enabled_wrapper
|
||||
|
||||
if decorated_function:
|
||||
return curry_wrapper(decorated_function)
|
||||
else:
|
||||
return curry_wrapper
|
28
src/oauth2client/contrib/django_util/signals.py
Normal file
28
src/oauth2client/contrib/django_util/signals.py
Normal file
@ -0,0 +1,28 @@
|
||||
# Copyright 2015 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
""" Signals for Google OAuth2 Helper
|
||||
|
||||
This module contains signals for Google OAuth2 Helper. Currently it only
|
||||
contains one, which fires when an OAuth2 authorization flow has completed.
|
||||
"""
|
||||
|
||||
import django.dispatch
|
||||
|
||||
"""Signal that fires when OAuth2 Flow has completed.
|
||||
It passes the Django request object and the OAuth2 credentials object to the
|
||||
receiver.
|
||||
"""
|
||||
oauth2_authorized = django.dispatch.Signal(
|
||||
providing_args=["request", "credentials"])
|
23
src/oauth2client/contrib/django_util/site.py
Normal file
23
src/oauth2client/contrib/django_util/site.py
Normal file
@ -0,0 +1,23 @@
|
||||
# Copyright 2015 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from django.conf import urls
|
||||
from oauth2client.contrib.django_util import views
|
||||
|
||||
urlpatterns = [
|
||||
urls.url(r'oauth2callback/', views.oauth2_callback, name="callback"),
|
||||
urls.url(r'oauth2authorize/', views.oauth2_authorize, name="authorize")
|
||||
]
|
||||
|
||||
urls = (urlpatterns, "google_oauth", "google_oauth")
|
27
src/oauth2client/contrib/django_util/storage.py
Normal file
27
src/oauth2client/contrib/django_util/storage.py
Normal file
@ -0,0 +1,27 @@
|
||||
# Copyright 2015 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from oauth2client.contrib.dictionary_storage import DictionaryStorage
|
||||
|
||||
_CREDENTIALS_KEY = 'google_oauth2_credentials'
|
||||
|
||||
|
||||
def get_storage(request):
|
||||
# TODO(issue 319): Make this pluggable with different storage providers
|
||||
# https://github.com/google/oauth2client/issues/319
|
||||
""" Gets a Credentials storage object for the Django OAuth2 Helper object
|
||||
:param request: Reference to the current request object
|
||||
:return: A OAuth2Client Storage implementation based on sessions
|
||||
"""
|
||||
return DictionaryStorage(request.session, key=_CREDENTIALS_KEY)
|
139
src/oauth2client/contrib/django_util/views.py
Normal file
139
src/oauth2client/contrib/django_util/views.py
Normal file
@ -0,0 +1,139 @@
|
||||
# Copyright 2015 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import pickle
|
||||
from django import http
|
||||
from django.core import urlresolvers
|
||||
from django import shortcuts
|
||||
from oauth2client import client
|
||||
from oauth2client.contrib import django_util
|
||||
from oauth2client.contrib.django_util import signals
|
||||
from oauth2client.contrib.django_util import storage
|
||||
|
||||
_CSRF_KEY = 'google_oauth2_csrf_token'
|
||||
_FLOW_KEY = 'google_oauth2_flow_{0}'
|
||||
|
||||
|
||||
def _make_flow(request, scopes, return_url=None):
|
||||
"""Creates a Web Server Flow"""
|
||||
# Generate a CSRF token to prevent malicious requests.
|
||||
csrf_token = hashlib.sha256(os.urandom(1024)).hexdigest()
|
||||
|
||||
request.session[_CSRF_KEY] = csrf_token
|
||||
|
||||
state = json.dumps({
|
||||
'csrf_token': csrf_token,
|
||||
'return_url': return_url,
|
||||
})
|
||||
|
||||
flow = client.OAuth2WebServerFlow(
|
||||
client_id=django_util.oauth2_settings.client_id,
|
||||
client_secret=django_util.oauth2_settings.client_secret,
|
||||
scope=scopes,
|
||||
state=state,
|
||||
redirect_uri=request.build_absolute_uri(
|
||||
urlresolvers.reverse("google_oauth:callback")))
|
||||
|
||||
flow_key = _FLOW_KEY.format(csrf_token)
|
||||
request.session[flow_key] = pickle.dumps(flow)
|
||||
return flow
|
||||
|
||||
|
||||
def _get_flow_for_token(csrf_token, request):
|
||||
""" Looks up the flow in session to recover information about requested
|
||||
scopes."""
|
||||
flow_pickle = request.session.get(_FLOW_KEY.format(csrf_token), None)
|
||||
return None if flow_pickle is None else pickle.loads(flow_pickle)
|
||||
|
||||
|
||||
def oauth2_callback(request):
|
||||
""" View that handles the user's return from OAuth2 provider.
|
||||
|
||||
This view verifies the CSRF state and OAuth authorization code, and on
|
||||
success stores the credentials obtained in the storage provider,
|
||||
and redirects to the return_url specified in the authorize view and
|
||||
stored in the session.
|
||||
|
||||
:param request: Django request
|
||||
:return: A redirect response back to the return_url
|
||||
"""
|
||||
if 'error' in request.GET:
|
||||
reason = request.GET.get(
|
||||
'error_description', request.GET.get('error', ''))
|
||||
return http.HttpResponseBadRequest(
|
||||
'Authorization failed %s' % reason)
|
||||
|
||||
try:
|
||||
encoded_state = request.GET['state']
|
||||
code = request.GET['code']
|
||||
except KeyError:
|
||||
return http.HttpResponseBadRequest(
|
||||
"Request missing state or authorization code")
|
||||
|
||||
try:
|
||||
server_csrf = request.session[_CSRF_KEY]
|
||||
except KeyError:
|
||||
return http.HttpResponseBadRequest("No existing session for this flow.")
|
||||
|
||||
try:
|
||||
state = json.loads(encoded_state)
|
||||
client_csrf = state['csrf_token']
|
||||
return_url = state['return_url']
|
||||
except (ValueError, KeyError):
|
||||
return http.HttpResponseBadRequest('Invalid state parameter.')
|
||||
|
||||
if client_csrf != server_csrf:
|
||||
return http.HttpResponseBadRequest('Invalid CSRF token.')
|
||||
|
||||
flow = _get_flow_for_token(client_csrf, request)
|
||||
|
||||
if not flow:
|
||||
return http.HttpResponseBadRequest("Missing Oauth2 flow.")
|
||||
|
||||
try:
|
||||
credentials = flow.step2_exchange(code)
|
||||
except client.FlowExchangeError as exchange_error:
|
||||
return http.HttpResponseBadRequest(
|
||||
"An error has occurred: {0}".format(exchange_error))
|
||||
|
||||
storage.get_storage(request).put(credentials)
|
||||
|
||||
signals.oauth2_authorized.send(sender=signals.oauth2_authorized,
|
||||
request=request, credentials=credentials)
|
||||
return shortcuts.redirect(return_url)
|
||||
|
||||
|
||||
def oauth2_authorize(request):
|
||||
""" View to start the OAuth2 Authorization flow
|
||||
|
||||
This view starts the OAuth2 authorization flow. If scopes is passed in
|
||||
as a GET URL parameter, it will authorize those scopes, otherwise the
|
||||
default scopes specified in settings. The return_url can also be
|
||||
specified as a GET parameter, otherwise the referer header will be
|
||||
checked, and if that isn't found it will return to the root path.
|
||||
|
||||
:param request: The Django request object
|
||||
:return: A redirect to Google OAuth2 Authorization
|
||||
"""
|
||||
scopes = request.GET.getlist('scopes', django_util.oauth2_settings.scopes)
|
||||
return_url = request.GET.get('return_url', None)
|
||||
|
||||
if not return_url:
|
||||
return_url = request.META.get('HTTP_REFERER', '/')
|
||||
flow = _make_flow(request=request, scopes=scopes, return_url=return_url)
|
||||
auth_url = flow.step1_get_authorize_url()
|
||||
return shortcuts.redirect(auth_url)
|
@ -23,22 +23,23 @@ available.
|
||||
Configuration
|
||||
=============
|
||||
|
||||
To configure, you'll need a set of OAuth2 client ID from the
|
||||
To configure, you'll need a set of OAuth2 web application credentials from the
|
||||
`Google Developer's Console <https://console.developers.google.com/project/_/\
|
||||
apiui/credential>`__.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from oauth2client.flask_util import UserOAuth2
|
||||
from oauth2client.contrib.flask_util import UserOAuth2
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
app.config['SECRET_KEY'] = 'your-secret-key'
|
||||
|
||||
app.config['OAUTH2_CLIENT_SECRETS_JSON'] = 'client_secrets.json'
|
||||
app.config['GOOGLE_OAUTH2_CLIENT_SECRETS_JSON'] = 'client_secrets.json'
|
||||
|
||||
# or, specify the client id and secret separately
|
||||
app.config['OAUTH2_CLIENT_ID'] = 'your-client-id'
|
||||
app.config['OAUTH2_CLIENT_SECRET'] = 'your-client-secret'
|
||||
app.config['GOOGLE_OAUTH2_CLIENT_ID'] = 'your-client-id'
|
||||
app.config['GOOGLE_OAUTH2_CLIENT_SECRET'] = 'your-client-secret'
|
||||
|
||||
oauth2 = UserOAuth2(app)
|
||||
|
||||
@ -164,6 +165,7 @@ available outside of a request context, you will need to implement your own
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import pickle
|
||||
from functools import wraps
|
||||
|
||||
import six.moves.http_client as httplib
|
||||
@ -181,16 +183,29 @@ except ImportError: # pragma: NO COVER
|
||||
raise ImportError('The flask utilities require flask 0.9 or newer.')
|
||||
|
||||
from oauth2client.client import FlowExchangeError
|
||||
from oauth2client.client import OAuth2Credentials
|
||||
from oauth2client.client import OAuth2WebServerFlow
|
||||
from oauth2client.client import Storage
|
||||
from oauth2client.contrib.dictionary_storage import DictionaryStorage
|
||||
from oauth2client import clientsecrets
|
||||
from oauth2client import util
|
||||
|
||||
|
||||
__author__ = 'jonwayne@google.com (Jon Wayne Parrott)'
|
||||
|
||||
DEFAULT_SCOPES = ('email',)
|
||||
_DEFAULT_SCOPES = ('email',)
|
||||
_CREDENTIALS_KEY = 'google_oauth2_credentials'
|
||||
_FLOW_KEY = 'google_oauth2_flow_{0}'
|
||||
_CSRF_KEY = 'google_oauth2_csrf_token'
|
||||
|
||||
|
||||
def _get_flow_for_token(csrf_token):
|
||||
"""Retrieves the flow instance associated with a given CSRF token from
|
||||
the Flask session."""
|
||||
flow_pickle = session.get(
|
||||
_FLOW_KEY.format(csrf_token), None)
|
||||
|
||||
if flow_pickle is None:
|
||||
return None
|
||||
else:
|
||||
return pickle.loads(flow_pickle)
|
||||
|
||||
|
||||
class UserOAuth2(object):
|
||||
@ -202,10 +217,11 @@ class UserOAuth2(object):
|
||||
file, obtained from the credentials screen in the Google Developers
|
||||
console.
|
||||
* ``GOOGLE_OAUTH2_CLIENT_ID`` the oauth2 credentials' client ID. This
|
||||
is only needed if ``OAUTH2_CLIENT_SECRETS_JSON`` is not specified.
|
||||
* ``GOOGLE_OAUTH2_CLIENT_SECRET`` the oauth2 credentials' client
|
||||
secret. This is only needed if ``OAUTH2_CLIENT_SECRETS_JSON`` is not
|
||||
is only needed if ``GOOGLE_OAUTH2_CLIENT_SECRETS_JSON`` is not
|
||||
specified.
|
||||
* ``GOOGLE_OAUTH2_CLIENT_SECRET`` the oauth2 credentials' client
|
||||
secret. This is only needed if ``GOOGLE_OAUTH2_CLIENT_SECRETS_JSON``
|
||||
is not specified.
|
||||
|
||||
If app is specified, all arguments will be passed along to init_app.
|
||||
|
||||
@ -227,7 +243,8 @@ class UserOAuth2(object):
|
||||
app: A Flask application.
|
||||
scopes: Optional list of scopes to authorize.
|
||||
client_secrets_file: Path to a file containing client secrets. You
|
||||
can also specify the OAUTH2_CLIENT_SECRETS_JSON config value.
|
||||
can also specify the GOOGLE_OAUTH2_CLIENT_SECRETS_JSON config
|
||||
value.
|
||||
client_id: If not specifying a client secrets file, specify the
|
||||
OAuth2 client id. You can also specify the
|
||||
GOOGLE_OAUTH2_CLIENT_ID config value. You must also provide a
|
||||
@ -246,11 +263,11 @@ class UserOAuth2(object):
|
||||
self.flow_kwargs = kwargs
|
||||
|
||||
if storage is None:
|
||||
storage = FlaskSessionStorage()
|
||||
storage = DictionaryStorage(session, key=_CREDENTIALS_KEY)
|
||||
self.storage = storage
|
||||
|
||||
if scopes is None:
|
||||
scopes = app.config.get('GOOGLE_OAUTH2_SCOPES', DEFAULT_SCOPES)
|
||||
scopes = app.config.get('GOOGLE_OAUTH2_SCOPES', _DEFAULT_SCOPES)
|
||||
self.scopes = scopes
|
||||
|
||||
self._load_config(client_secrets_file, client_id, client_secret)
|
||||
@ -300,7 +317,8 @@ class UserOAuth2(object):
|
||||
client_type, client_info = clientsecrets.loadfile(filename)
|
||||
if client_type != clientsecrets.TYPE_WEB:
|
||||
raise ValueError(
|
||||
'The flow specified in %s is not supported.' % client_type)
|
||||
'The flow specified in {0} is not supported.'.format(
|
||||
client_type))
|
||||
|
||||
self.client_id = client_info['client_id']
|
||||
self.client_secret = client_info['client_secret']
|
||||
@ -310,7 +328,7 @@ class UserOAuth2(object):
|
||||
# Generate a CSRF token to prevent malicious requests.
|
||||
csrf_token = hashlib.sha256(os.urandom(1024)).hexdigest()
|
||||
|
||||
session['google_oauth2_csrf_token'] = csrf_token
|
||||
session[_CSRF_KEY] = csrf_token
|
||||
|
||||
state = json.dumps({
|
||||
'csrf_token': csrf_token,
|
||||
@ -320,10 +338,10 @@ class UserOAuth2(object):
|
||||
kw = self.flow_kwargs.copy()
|
||||
kw.update(kwargs)
|
||||
|
||||
extra_scopes = util.scopes_to_string(kw.pop('scopes', ''))
|
||||
scopes = ' '.join([util.scopes_to_string(self.scopes), extra_scopes])
|
||||
extra_scopes = kw.pop('scopes', [])
|
||||
scopes = set(self.scopes).union(set(extra_scopes))
|
||||
|
||||
return OAuth2WebServerFlow(
|
||||
flow = OAuth2WebServerFlow(
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
scope=scopes,
|
||||
@ -331,6 +349,11 @@ class UserOAuth2(object):
|
||||
redirect_uri=url_for('oauth2.callback', _external=True),
|
||||
**kw)
|
||||
|
||||
flow_key = _FLOW_KEY.format(csrf_token)
|
||||
session[flow_key] = pickle.dumps(flow)
|
||||
|
||||
return flow
|
||||
|
||||
def _create_blueprint(self):
|
||||
bp = Blueprint('oauth2', __name__)
|
||||
bp.add_url_rule('/oauth2authorize', 'authorize', self.authorize_view)
|
||||
@ -367,11 +390,12 @@ class UserOAuth2(object):
|
||||
if 'error' in request.args:
|
||||
reason = request.args.get(
|
||||
'error_description', request.args.get('error', ''))
|
||||
return 'Authorization failed: %s' % reason, httplib.BAD_REQUEST
|
||||
return ('Authorization failed: {0}'.format(reason),
|
||||
httplib.BAD_REQUEST)
|
||||
|
||||
try:
|
||||
encoded_state = request.args['state']
|
||||
server_csrf = session['google_oauth2_csrf_token']
|
||||
server_csrf = session[_CSRF_KEY]
|
||||
code = request.args['code']
|
||||
except KeyError:
|
||||
return 'Invalid request', httplib.BAD_REQUEST
|
||||
@ -386,14 +410,17 @@ class UserOAuth2(object):
|
||||
if client_csrf != server_csrf:
|
||||
return 'Invalid request state', httplib.BAD_REQUEST
|
||||
|
||||
flow = self._make_flow()
|
||||
flow = _get_flow_for_token(server_csrf)
|
||||
|
||||
if flow is None:
|
||||
return 'Invalid request state', httplib.BAD_REQUEST
|
||||
|
||||
# Exchange the auth code for credentials.
|
||||
try:
|
||||
credentials = flow.step2_exchange(code)
|
||||
except FlowExchangeError as exchange_error:
|
||||
current_app.logger.exception(exchange_error)
|
||||
content = 'An error occurred: %s' % (exchange_error,)
|
||||
content = 'An error occurred: {0}'.format(exchange_error)
|
||||
return content, httplib.BAD_REQUEST
|
||||
|
||||
# Save the credentials to the storage.
|
||||
@ -409,7 +436,7 @@ class UserOAuth2(object):
|
||||
"""The credentials for the current user or None if unavailable."""
|
||||
ctx = _app_ctx_stack.top
|
||||
|
||||
if not hasattr(ctx, 'google_oauth2_credentials'):
|
||||
if not hasattr(ctx, _CREDENTIALS_KEY):
|
||||
ctx.google_oauth2_credentials = self.storage.get()
|
||||
|
||||
return ctx.google_oauth2_credentials
|
||||
@ -432,7 +459,7 @@ class UserOAuth2(object):
|
||||
return self.credentials.id_token['email']
|
||||
except KeyError:
|
||||
current_app.logger.error(
|
||||
'Invalid id_token %s', self.credentials.id_token)
|
||||
'Invalid id_token {0}'.format(self.credentials.id_token))
|
||||
|
||||
@property
|
||||
def user_id(self):
|
||||
@ -448,7 +475,7 @@ class UserOAuth2(object):
|
||||
return self.credentials.id_token['sub']
|
||||
except KeyError:
|
||||
current_app.logger.error(
|
||||
'Invalid id_token %s', self.credentials.id_token)
|
||||
'Invalid id_token {0}'.format(self.credentials.id_token))
|
||||
|
||||
def authorize_url(self, return_url, **kwargs):
|
||||
"""Creates a URL that can be used to start the authorization flow.
|
||||
@ -473,28 +500,30 @@ class UserOAuth2(object):
|
||||
def curry_wrapper(wrapped_function):
|
||||
@wraps(wrapped_function)
|
||||
def required_wrapper(*args, **kwargs):
|
||||
|
||||
return_url = decorator_kwargs.pop('return_url', request.url)
|
||||
|
||||
# No credentials, redirect for new authorization.
|
||||
if not self.has_credentials():
|
||||
requested_scopes = set(self.scopes)
|
||||
if scopes is not None:
|
||||
requested_scopes |= set(scopes)
|
||||
if self.has_credentials():
|
||||
requested_scopes |= self.credentials.scopes
|
||||
|
||||
requested_scopes = list(requested_scopes)
|
||||
|
||||
# Does the user have credentials and does the credentials have
|
||||
# all of the needed scopes?
|
||||
if (self.has_credentials() and
|
||||
self.credentials.has_scopes(requested_scopes)):
|
||||
return wrapped_function(*args, **kwargs)
|
||||
# Otherwise, redirect to authorization
|
||||
else:
|
||||
auth_url = self.authorize_url(
|
||||
return_url,
|
||||
scopes=scopes,
|
||||
scopes=requested_scopes,
|
||||
**decorator_kwargs)
|
||||
return redirect(auth_url)
|
||||
|
||||
# Existing credentials but mismatching scopes, redirect for
|
||||
# incremental authorization.
|
||||
if scopes and not self.credentials.has_scopes(scopes):
|
||||
auth_url = self.authorize_url(
|
||||
return_url,
|
||||
scopes=list(self.credentials.scopes) + scopes,
|
||||
**decorator_kwargs)
|
||||
return redirect(auth_url)
|
||||
|
||||
return wrapped_function(*args, **kwargs)
|
||||
|
||||
return required_wrapper
|
||||
|
||||
if decorated_function:
|
||||
@ -518,31 +547,3 @@ class UserOAuth2(object):
|
||||
if not self.credentials:
|
||||
raise ValueError('No credentials available.')
|
||||
return self.credentials.authorize(httplib2.Http(*args, **kwargs))
|
||||
|
||||
|
||||
class FlaskSessionStorage(Storage):
|
||||
"""Storage implementation that uses Flask sessions.
|
||||
|
||||
Note that flask's default sessions are signed but not encrypted. Users
|
||||
can see their own credentials and non-https connections can intercept user
|
||||
credentials. We strongly recommend using a server-side session
|
||||
implementation.
|
||||
"""
|
||||
|
||||
def locked_get(self):
|
||||
serialized = session.get('google_oauth2_credentials')
|
||||
|
||||
if serialized is None:
|
||||
return None
|
||||
|
||||
credentials = OAuth2Credentials.from_json(serialized)
|
||||
credentials.set_store(self)
|
||||
|
||||
return credentials
|
||||
|
||||
def locked_put(self, credentials):
|
||||
session['google_oauth2_credentials'] = credentials.to_json()
|
||||
|
||||
def locked_delete(self):
|
||||
if 'google_oauth2_credentials' in session:
|
||||
del session['google_oauth2_credentials']
|
194
src/oauth2client/contrib/gce.py
Normal file
194
src/oauth2client/contrib/gce.py
Normal file
@ -0,0 +1,194 @@
|
||||
# Copyright 2014 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Utilities for Google Compute Engine
|
||||
|
||||
Utilities for making it easier to use OAuth 2.0 on Google Compute Engine.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import warnings
|
||||
|
||||
import httplib2
|
||||
from six.moves import http_client
|
||||
from six.moves import urllib
|
||||
|
||||
from oauth2client._helpers import _from_bytes
|
||||
from oauth2client import util
|
||||
from oauth2client.client import HttpAccessTokenRefreshError
|
||||
from oauth2client.client import AssertionCredentials
|
||||
|
||||
|
||||
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# URI Template for the endpoint that returns access_tokens.
|
||||
_METADATA_ROOT = ('http://metadata.google.internal/computeMetadata/v1/'
|
||||
'instance/service-accounts/default/')
|
||||
META = _METADATA_ROOT + 'token'
|
||||
_DEFAULT_EMAIL_METADATA = _METADATA_ROOT + 'email'
|
||||
_SCOPES_WARNING = """\
|
||||
You have requested explicit scopes to be used with a GCE service account.
|
||||
Using this argument will have no effect on the actual scopes for tokens
|
||||
requested. These scopes are set at VM instance creation time and
|
||||
can't be overridden in the request.
|
||||
"""
|
||||
|
||||
|
||||
def _get_service_account_email(http_request=None):
|
||||
"""Get the GCE service account email from the current environment.
|
||||
|
||||
Args:
|
||||
http_request: callable, (Optional) a callable that matches the method
|
||||
signature of httplib2.Http.request, used to make
|
||||
the request to the metadata service.
|
||||
|
||||
Returns:
|
||||
tuple, A pair where the first entry is an optional response (from a
|
||||
failed request) and the second is service account email found (as
|
||||
a string).
|
||||
"""
|
||||
if http_request is None:
|
||||
http_request = httplib2.Http().request
|
||||
response, content = http_request(
|
||||
_DEFAULT_EMAIL_METADATA, headers={'Metadata-Flavor': 'Google'})
|
||||
if response.status == http_client.OK:
|
||||
content = _from_bytes(content)
|
||||
return None, content
|
||||
else:
|
||||
return response, content
|
||||
|
||||
|
||||
class AppAssertionCredentials(AssertionCredentials):
|
||||
"""Credentials object for Compute Engine Assertion Grants
|
||||
|
||||
This object will allow a Compute Engine instance to identify itself to
|
||||
Google and other OAuth 2.0 servers that can verify assertions. It can be
|
||||
used for the purpose of accessing data stored under an account assigned to
|
||||
the Compute Engine instance itself.
|
||||
|
||||
This credential does not require a flow to instantiate because it
|
||||
represents a two legged flow, and therefore has all of the required
|
||||
information to generate and refresh its own access tokens.
|
||||
"""
|
||||
|
||||
@util.positional(2)
|
||||
def __init__(self, scope='', **kwargs):
|
||||
"""Constructor for AppAssertionCredentials
|
||||
|
||||
Args:
|
||||
scope: string or iterable of strings, scope(s) of the credentials
|
||||
being requested. Using this argument will have no effect on
|
||||
the actual scopes for tokens requested. These scopes are
|
||||
set at VM instance creation time and won't change.
|
||||
"""
|
||||
if scope:
|
||||
warnings.warn(_SCOPES_WARNING)
|
||||
# This is just provided for backwards compatibility, but is not
|
||||
# used by this class.
|
||||
self.scope = util.scopes_to_string(scope)
|
||||
self.kwargs = kwargs
|
||||
|
||||
# Assertion type is no longer used, but still in the
|
||||
# parent class signature.
|
||||
super(AppAssertionCredentials, self).__init__(None)
|
||||
self._service_account_email = None
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, json_data):
|
||||
data = json.loads(_from_bytes(json_data))
|
||||
return AppAssertionCredentials(data['scope'])
|
||||
|
||||
def _refresh(self, http_request):
|
||||
"""Refreshes the access_token.
|
||||
|
||||
Skip all the storage hoops and just refresh using the API.
|
||||
|
||||
Args:
|
||||
http_request: callable, a callable that matches the method
|
||||
signature of httplib2.Http.request, used to make
|
||||
the refresh request.
|
||||
|
||||
Raises:
|
||||
HttpAccessTokenRefreshError: When the refresh fails.
|
||||
"""
|
||||
response, content = http_request(
|
||||
META, headers={'Metadata-Flavor': 'Google'})
|
||||
content = _from_bytes(content)
|
||||
if response.status == http_client.OK:
|
||||
try:
|
||||
token_content = json.loads(content)
|
||||
except Exception as e:
|
||||
raise HttpAccessTokenRefreshError(str(e),
|
||||
status=response.status)
|
||||
self.access_token = token_content['access_token']
|
||||
else:
|
||||
if response.status == http_client.NOT_FOUND:
|
||||
content += (' This can occur if a VM was created'
|
||||
' with no service account or scopes.')
|
||||
raise HttpAccessTokenRefreshError(content, status=response.status)
|
||||
|
||||
@property
|
||||
def serialization_data(self):
|
||||
raise NotImplementedError(
|
||||
'Cannot serialize credentials for GCE service accounts.')
|
||||
|
||||
def create_scoped_required(self):
|
||||
return False
|
||||
|
||||
def create_scoped(self, scopes):
|
||||
return AppAssertionCredentials(scopes, **self.kwargs)
|
||||
|
||||
def sign_blob(self, blob):
|
||||
"""Cryptographically sign a blob (of bytes).
|
||||
|
||||
This method is provided to support a common interface, but
|
||||
the actual key used for a Google Compute Engine service account
|
||||
is not available, so it can't be used to sign content.
|
||||
|
||||
Args:
|
||||
blob: bytes, Message to be signed.
|
||||
|
||||
Raises:
|
||||
NotImplementedError, always.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
'Compute Engine service accounts cannot sign blobs')
|
||||
|
||||
@property
|
||||
def service_account_email(self):
|
||||
"""Get the email for the current service account.
|
||||
|
||||
Uses the Google Compute Engine metadata service to retrieve the email
|
||||
of the default service account.
|
||||
|
||||
Returns:
|
||||
string, The email associated with the Google Compute Engine
|
||||
service account.
|
||||
|
||||
Raises:
|
||||
AttributeError, if the email can not be retrieved from the Google
|
||||
Compute Engine metadata service.
|
||||
"""
|
||||
if self._service_account_email is None:
|
||||
failure, email = _get_service_account_email()
|
||||
if failure is None:
|
||||
self._service_account_email = email
|
||||
else:
|
||||
raise AttributeError('Failed to retrieve the email from the '
|
||||
'Google Compute Engine metadata service',
|
||||
failure, email)
|
||||
return self._service_account_email
|
@ -59,24 +59,9 @@ class Storage(BaseStorage):
|
||||
credentials are stored.
|
||||
user_name: string, The name of the user to store credentials for.
|
||||
"""
|
||||
super(Storage, self).__init__(lock=threading.Lock())
|
||||
self._service_name = service_name
|
||||
self._user_name = user_name
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def acquire_lock(self):
|
||||
"""Acquires any lock necessary to access this Storage.
|
||||
|
||||
This lock is not reentrant.
|
||||
"""
|
||||
self._lock.acquire()
|
||||
|
||||
def release_lock(self):
|
||||
"""Release the Storage lock.
|
||||
|
||||
Trying to release a lock that isn't held will result in a
|
||||
RuntimeError.
|
||||
"""
|
||||
self._lock.release()
|
||||
|
||||
def locked_get(self):
|
||||
"""Retrieve Credential from file.
|
@ -53,7 +53,7 @@ import threading
|
||||
from oauth2client.client import Credentials
|
||||
from oauth2client.client import Storage as BaseStorage
|
||||
from oauth2client import util
|
||||
from oauth2client.locked_file import LockedFile
|
||||
from oauth2client.contrib.locked_file import LockedFile
|
||||
|
||||
|
||||
__author__ = 'jbeda@google.com (Joe Beda)'
|
||||
@ -73,6 +73,21 @@ class NewerCredentialStoreError(Error):
|
||||
"""The credential store is a newer version than supported."""
|
||||
|
||||
|
||||
def _dict_to_tuple_key(dictionary):
|
||||
"""Converts a dictionary to a tuple that can be used as an immutable key.
|
||||
|
||||
The resulting key is always sorted so that logically equivalent
|
||||
dictionaries always produce an identical tuple for a key.
|
||||
|
||||
Args:
|
||||
dictionary: the dictionary to use as the key.
|
||||
|
||||
Returns:
|
||||
A tuple representing the dictionary in it's naturally sorted ordering.
|
||||
"""
|
||||
return tuple(sorted(dictionary.items()))
|
||||
|
||||
|
||||
@util.positional(4)
|
||||
def get_credential_storage(filename, client_id, user_agent, scope,
|
||||
warn_on_readonly=True):
|
||||
@ -139,7 +154,7 @@ def get_credential_storage_custom_key(filename, key_dict,
|
||||
credential.
|
||||
"""
|
||||
multistore = _get_multistore(filename, warn_on_readonly=warn_on_readonly)
|
||||
key = util.dict_to_tuple_key(key_dict)
|
||||
key = _dict_to_tuple_key(key_dict)
|
||||
return multistore._get_storage(key)
|
||||
|
||||
|
||||
@ -290,6 +305,11 @@ class _MultiStore(object):
|
||||
elif e.errno == errno.ENOLCK:
|
||||
logger.warn('File system is out of resources for writing the '
|
||||
'credentials file (is your disk full?).')
|
||||
elif e.errno == errno.EDEADLK:
|
||||
logger.warn('Lock contention on multistore file, opening '
|
||||
'in read-only mode.')
|
||||
elif e.errno == errno.EACCES:
|
||||
logger.warn('Cannot access credentials file.')
|
||||
else:
|
||||
raise
|
||||
if not self._file.is_locked():
|
||||
@ -399,7 +419,7 @@ class _MultiStore(object):
|
||||
OAuth2Credential object.
|
||||
"""
|
||||
raw_key = cred_entry['key']
|
||||
key = util.dict_to_tuple_key(raw_key)
|
||||
key = _dict_to_tuple_key(raw_key)
|
||||
credential = None
|
||||
credential = Credentials.new_from_json(
|
||||
json.dumps(cred_entry['credential']))
|
@ -1,4 +1,3 @@
|
||||
#
|
||||
# Copyright 2014 the Melange authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@ -18,7 +17,6 @@
|
||||
import base64
|
||||
import binascii
|
||||
import hmac
|
||||
import six
|
||||
import time
|
||||
|
||||
from oauth2client._helpers import _to_bytes
|
@ -24,6 +24,8 @@ from oauth2client._helpers import _json_encode
|
||||
from oauth2client._helpers import _to_bytes
|
||||
from oauth2client._helpers import _urlsafe_b64decode
|
||||
from oauth2client._helpers import _urlsafe_b64encode
|
||||
from oauth2client._pure_python_crypt import RsaSigner
|
||||
from oauth2client._pure_python_crypt import RsaVerifier
|
||||
|
||||
|
||||
CLOCK_SKEW_SECS = 300 # 5 minutes in seconds
|
||||
@ -65,11 +67,11 @@ elif PyCryptoSigner: # pragma: NO COVER
|
||||
Signer = PyCryptoSigner
|
||||
Verifier = PyCryptoVerifier
|
||||
else: # pragma: NO COVER
|
||||
raise ImportError('No encryption library found. Please install either '
|
||||
'PyOpenSSL, or PyCrypto 2.6 or later')
|
||||
Signer = RsaSigner
|
||||
Verifier = RsaVerifier
|
||||
|
||||
|
||||
def make_signed_jwt(signer, payload):
|
||||
def make_signed_jwt(signer, payload, key_id=None):
|
||||
"""Make a signed JWT.
|
||||
|
||||
See http://self-issued.info/docs/draft-jones-json-web-token.html.
|
||||
@ -77,15 +79,18 @@ def make_signed_jwt(signer, payload):
|
||||
Args:
|
||||
signer: crypt.Signer, Cryptographic signer.
|
||||
payload: dict, Dictionary of data to convert to JSON and then sign.
|
||||
key_id: string, (Optional) Key ID header.
|
||||
|
||||
Returns:
|
||||
string, The JWT for the payload.
|
||||
"""
|
||||
header = {'typ': 'JWT', 'alg': 'RS256'}
|
||||
if key_id is not None:
|
||||
header['kid'] = key_id
|
||||
|
||||
segments = [
|
||||
_urlsafe_b64encode(_json_encode(header)),
|
||||
_urlsafe_b64encode(_json_encode(payload)),
|
||||
_urlsafe_b64encode(_json_encode(header)),
|
||||
_urlsafe_b64encode(_json_encode(payload)),
|
||||
]
|
||||
signing_input = b'.'.join(segments)
|
||||
|
||||
|
@ -36,29 +36,14 @@ class Storage(BaseStorage):
|
||||
"""Store and retrieve a single credential to and from a file."""
|
||||
|
||||
def __init__(self, filename):
|
||||
super(Storage, self).__init__(lock=threading.Lock())
|
||||
self._filename = filename
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def _validate_file(self):
|
||||
if os.path.islink(self._filename):
|
||||
raise CredentialsFileSymbolicLinkError(
|
||||
'File: %s is a symbolic link.' % self._filename)
|
||||
|
||||
def acquire_lock(self):
|
||||
"""Acquires any lock necessary to access this Storage.
|
||||
|
||||
This lock is not reentrant.
|
||||
"""
|
||||
self._lock.acquire()
|
||||
|
||||
def release_lock(self):
|
||||
"""Release the Storage lock.
|
||||
|
||||
Trying to release a lock that isn't held will result in a
|
||||
RuntimeError.
|
||||
"""
|
||||
self._lock.release()
|
||||
|
||||
def locked_get(self):
|
||||
"""Retrieve Credential from file.
|
||||
|
||||
|
@ -1,110 +0,0 @@
|
||||
# Copyright 2014 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Utilities for Google Compute Engine
|
||||
|
||||
Utilities for making it easier to use OAuth 2.0 on Google Compute Engine.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from six.moves import urllib
|
||||
|
||||
from oauth2client._helpers import _from_bytes
|
||||
from oauth2client import util
|
||||
from oauth2client.client import AccessTokenRefreshError
|
||||
from oauth2client.client import AssertionCredentials
|
||||
|
||||
|
||||
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# URI Template for the endpoint that returns access_tokens.
|
||||
META = ('http://metadata.google.internal/0.1/meta-data/service-accounts/'
|
||||
'default/acquire{?scope}')
|
||||
|
||||
|
||||
class AppAssertionCredentials(AssertionCredentials):
|
||||
"""Credentials object for Compute Engine Assertion Grants
|
||||
|
||||
This object will allow a Compute Engine instance to identify itself to
|
||||
Google and other OAuth 2.0 servers that can verify assertions. It can be
|
||||
used for the purpose of accessing data stored under an account assigned to
|
||||
the Compute Engine instance itself.
|
||||
|
||||
This credential does not require a flow to instantiate because it
|
||||
represents a two legged flow, and therefore has all of the required
|
||||
information to generate and refresh its own access tokens.
|
||||
"""
|
||||
|
||||
@util.positional(2)
|
||||
def __init__(self, scope, **kwargs):
|
||||
"""Constructor for AppAssertionCredentials
|
||||
|
||||
Args:
|
||||
scope: string or iterable of strings, scope(s) of the credentials
|
||||
being requested.
|
||||
"""
|
||||
self.scope = util.scopes_to_string(scope)
|
||||
self.kwargs = kwargs
|
||||
|
||||
# Assertion type is no longer used, but still in the
|
||||
# parent class signature.
|
||||
super(AppAssertionCredentials, self).__init__(None)
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, json_data):
|
||||
data = json.loads(_from_bytes(json_data))
|
||||
return AppAssertionCredentials(data['scope'])
|
||||
|
||||
def _refresh(self, http_request):
|
||||
"""Refreshes the access_token.
|
||||
|
||||
Skip all the storage hoops and just refresh using the API.
|
||||
|
||||
Args:
|
||||
http_request: callable, a callable that matches the method
|
||||
signature of httplib2.Http.request, used to make
|
||||
the refresh request.
|
||||
|
||||
Raises:
|
||||
AccessTokenRefreshError: When the refresh fails.
|
||||
"""
|
||||
query = '?scope=%s' % urllib.parse.quote(self.scope, '')
|
||||
uri = META.replace('{?scope}', query)
|
||||
response, content = http_request(uri)
|
||||
content = _from_bytes(content)
|
||||
if response.status == 200:
|
||||
try:
|
||||
d = json.loads(content)
|
||||
except Exception as e:
|
||||
raise AccessTokenRefreshError(str(e))
|
||||
self.access_token = d['accessToken']
|
||||
else:
|
||||
if response.status == 404:
|
||||
content += (' This can occur if a VM was created'
|
||||
' with no service account or scopes.')
|
||||
raise AccessTokenRefreshError(content)
|
||||
|
||||
@property
|
||||
def serialization_data(self):
|
||||
raise NotImplementedError(
|
||||
'Cannot serialize credentials for GCE service accounts.')
|
||||
|
||||
def create_scoped_required(self):
|
||||
return not self.scope
|
||||
|
||||
def create_scoped(self, scopes):
|
||||
return AppAssertionCredentials(scopes, **self.kwargs)
|
@ -1,161 +0,0 @@
|
||||
# Copyright 2014 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""This module holds the old run() function which is deprecated, the
|
||||
tools.run_flow() function should be used in its place."""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import logging
|
||||
import socket
|
||||
import sys
|
||||
import webbrowser
|
||||
|
||||
import gflags
|
||||
from six.moves import input
|
||||
|
||||
from oauth2client import client
|
||||
from oauth2client import util
|
||||
from oauth2client.tools import ClientRedirectHandler
|
||||
from oauth2client.tools import ClientRedirectServer
|
||||
|
||||
|
||||
FLAGS = gflags.FLAGS
|
||||
|
||||
gflags.DEFINE_boolean('auth_local_webserver', True,
|
||||
('Run a local web server to handle redirects during '
|
||||
'OAuth authorization.'))
|
||||
|
||||
gflags.DEFINE_string('auth_host_name', 'localhost',
|
||||
('Host name to use when running a local web server to '
|
||||
'handle redirects during OAuth authorization.'))
|
||||
|
||||
gflags.DEFINE_multi_int('auth_host_port', [8080, 8090],
|
||||
('Port to use when running a local web server to '
|
||||
'handle redirects during OAuth authorization.'))
|
||||
|
||||
|
||||
@util.positional(2)
|
||||
def run(flow, storage, http=None):
|
||||
"""Core code for a command-line application.
|
||||
|
||||
The ``run()`` function is called from your application and runs
|
||||
through all the steps to obtain credentials. It takes a ``Flow``
|
||||
argument and attempts to open an authorization server page in the
|
||||
user's default web browser. The server asks the user to grant your
|
||||
application access to the user's data. If the user grants access,
|
||||
the ``run()`` function returns new credentials. The new credentials
|
||||
are also stored in the ``storage`` argument, which updates the file
|
||||
associated with the ``Storage`` object.
|
||||
|
||||
It presumes it is run from a command-line application and supports the
|
||||
following flags:
|
||||
|
||||
``--auth_host_name`` (string, default: ``localhost``)
|
||||
Host name to use when running a local web server to handle
|
||||
redirects during OAuth authorization.
|
||||
|
||||
``--auth_host_port`` (integer, default: ``[8080, 8090]``)
|
||||
Port to use when running a local web server to handle redirects
|
||||
during OAuth authorization. Repeat this option to specify a list
|
||||
of values.
|
||||
|
||||
``--[no]auth_local_webserver`` (boolean, default: ``True``)
|
||||
Run a local web server to handle redirects during OAuth authorization.
|
||||
|
||||
Since it uses flags make sure to initialize the ``gflags`` module before
|
||||
calling ``run()``.
|
||||
|
||||
Args:
|
||||
flow: Flow, an OAuth 2.0 Flow to step through.
|
||||
storage: Storage, a ``Storage`` to store the credential in.
|
||||
http: An instance of ``httplib2.Http.request`` or something that acts
|
||||
like it.
|
||||
|
||||
Returns:
|
||||
Credentials, the obtained credential.
|
||||
"""
|
||||
logging.warning('This function, oauth2client.tools.run(), and the use of '
|
||||
'the gflags library are deprecated and will be removed in a future '
|
||||
'version of the library.')
|
||||
if FLAGS.auth_local_webserver:
|
||||
success = False
|
||||
port_number = 0
|
||||
for port in FLAGS.auth_host_port:
|
||||
port_number = port
|
||||
try:
|
||||
httpd = ClientRedirectServer((FLAGS.auth_host_name, port),
|
||||
ClientRedirectHandler)
|
||||
except socket.error as e:
|
||||
pass
|
||||
else:
|
||||
success = True
|
||||
break
|
||||
FLAGS.auth_local_webserver = success
|
||||
if not success:
|
||||
print('Failed to start a local webserver listening on either port 8080')
|
||||
print('or port 9090. Please check your firewall settings and locally')
|
||||
print('running programs that may be blocking or using those ports.')
|
||||
print()
|
||||
print('Falling back to --noauth_local_webserver and continuing with')
|
||||
print('authorization.')
|
||||
print()
|
||||
|
||||
if FLAGS.auth_local_webserver:
|
||||
oauth_callback = 'http://%s:%s/' % (FLAGS.auth_host_name, port_number)
|
||||
else:
|
||||
oauth_callback = client.OOB_CALLBACK_URN
|
||||
flow.redirect_uri = oauth_callback
|
||||
authorize_url = flow.step1_get_authorize_url()
|
||||
|
||||
if FLAGS.auth_local_webserver:
|
||||
webbrowser.open(authorize_url, new=1, autoraise=True)
|
||||
print('Your browser has been opened to visit:')
|
||||
print()
|
||||
print(' ' + authorize_url)
|
||||
print()
|
||||
print('If your browser is on a different machine then exit and re-run')
|
||||
print('this application with the command-line parameter ')
|
||||
print()
|
||||
print(' --noauth_local_webserver')
|
||||
print()
|
||||
else:
|
||||
print('Go to the following link in your browser:')
|
||||
print()
|
||||
print(' ' + authorize_url)
|
||||
print()
|
||||
|
||||
code = None
|
||||
if FLAGS.auth_local_webserver:
|
||||
httpd.handle_request()
|
||||
if 'error' in httpd.query_params:
|
||||
sys.exit('Authentication request was rejected.')
|
||||
if 'code' in httpd.query_params:
|
||||
code = httpd.query_params['code']
|
||||
else:
|
||||
print('Failed to find "code" in the query parameters of the redirect.')
|
||||
sys.exit('Try running with --noauth_local_webserver.')
|
||||
else:
|
||||
code = input('Enter verification code: ').strip()
|
||||
|
||||
try:
|
||||
credential = flow.step2_exchange(code, http=http)
|
||||
except client.FlowExchangeError as e:
|
||||
sys.exit('Authentication has failed: %s' % e)
|
||||
|
||||
storage.put(credential)
|
||||
credential.set_store(storage)
|
||||
print('Authentication successful.')
|
||||
|
||||
return credential
|
@ -12,122 +12,452 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""A service account credentials class.
|
||||
|
||||
This credentials class is implemented on top of rsa library.
|
||||
"""
|
||||
"""oauth2client Service account credentials class."""
|
||||
|
||||
import base64
|
||||
import copy
|
||||
import datetime
|
||||
import json
|
||||
import time
|
||||
|
||||
from pyasn1.codec.ber import decoder
|
||||
from pyasn1_modules.rfc5208 import PrivateKeyInfo
|
||||
import rsa
|
||||
|
||||
from oauth2client import GOOGLE_REVOKE_URI
|
||||
from oauth2client import GOOGLE_TOKEN_URI
|
||||
from oauth2client._helpers import _json_encode
|
||||
from oauth2client._helpers import _to_bytes
|
||||
from oauth2client._helpers import _from_bytes
|
||||
from oauth2client._helpers import _urlsafe_b64encode
|
||||
from oauth2client import util
|
||||
from oauth2client.client import AssertionCredentials
|
||||
from oauth2client.client import EXPIRY_FORMAT
|
||||
from oauth2client.client import SERVICE_ACCOUNT
|
||||
from oauth2client import crypt
|
||||
|
||||
|
||||
class _ServiceAccountCredentials(AssertionCredentials):
|
||||
"""Class representing a service account (signed JWT) credential."""
|
||||
_PASSWORD_DEFAULT = 'notasecret'
|
||||
_PKCS12_KEY = '_private_key_pkcs12'
|
||||
_PKCS12_ERROR = r"""
|
||||
This library only implements PKCS#12 support via the pyOpenSSL library.
|
||||
Either install pyOpenSSL, or please convert the .p12 file
|
||||
to .pem format:
|
||||
$ cat key.p12 | \
|
||||
> openssl pkcs12 -nodes -nocerts -passin pass:notasecret | \
|
||||
> openssl rsa > key.pem
|
||||
"""
|
||||
|
||||
MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
|
||||
|
||||
def __init__(self, service_account_id, service_account_email,
|
||||
private_key_id, private_key_pkcs8_text, scopes,
|
||||
user_agent=None, token_uri=GOOGLE_TOKEN_URI,
|
||||
revoke_uri=GOOGLE_REVOKE_URI, **kwargs):
|
||||
class ServiceAccountCredentials(AssertionCredentials):
|
||||
"""Service Account credential for OAuth 2.0 signed JWT grants.
|
||||
|
||||
super(_ServiceAccountCredentials, self).__init__(
|
||||
None, user_agent=user_agent, token_uri=token_uri,
|
||||
revoke_uri=revoke_uri)
|
||||
Supports
|
||||
|
||||
* JSON keyfile (typically contains a PKCS8 key stored as
|
||||
PEM text)
|
||||
* ``.p12`` key (stores PKCS12 key and certificate)
|
||||
|
||||
Makes an assertion to server using a signed JWT assertion in exchange
|
||||
for an access token.
|
||||
|
||||
This credential does not require a flow to instantiate because it
|
||||
represents a two legged flow, and therefore has all of the required
|
||||
information to generate and refresh its own access tokens.
|
||||
|
||||
Args:
|
||||
service_account_email: string, The email associated with the
|
||||
service account.
|
||||
signer: ``crypt.Signer``, A signer which can be used to sign content.
|
||||
scopes: List or string, (Optional) Scopes to use when acquiring
|
||||
an access token.
|
||||
private_key_id: string, (Optional) Private key identifier. Typically
|
||||
only used with a JSON keyfile. Can be sent in the
|
||||
header of a JWT token assertion.
|
||||
client_id: string, (Optional) Client ID for the project that owns the
|
||||
service account.
|
||||
user_agent: string, (Optional) User agent to use when sending
|
||||
request.
|
||||
kwargs: dict, Extra key-value pairs (both strings) to send in the
|
||||
payload body when making an assertion.
|
||||
"""
|
||||
|
||||
MAX_TOKEN_LIFETIME_SECS = 3600
|
||||
"""Max lifetime of the token (one hour, in seconds)."""
|
||||
|
||||
NON_SERIALIZED_MEMBERS = (
|
||||
frozenset(['_signer']) |
|
||||
AssertionCredentials.NON_SERIALIZED_MEMBERS)
|
||||
"""Members that aren't serialized when object is converted to JSON."""
|
||||
|
||||
# Can be over-ridden by factory constructors. Used for
|
||||
# serialization/deserialization purposes.
|
||||
_private_key_pkcs8_pem = None
|
||||
_private_key_pkcs12 = None
|
||||
_private_key_password = None
|
||||
|
||||
def __init__(self,
|
||||
service_account_email,
|
||||
signer,
|
||||
scopes='',
|
||||
private_key_id=None,
|
||||
client_id=None,
|
||||
user_agent=None,
|
||||
**kwargs):
|
||||
|
||||
super(ServiceAccountCredentials, self).__init__(
|
||||
None, user_agent=user_agent)
|
||||
|
||||
self._service_account_id = service_account_id
|
||||
self._service_account_email = service_account_email
|
||||
self._private_key_id = private_key_id
|
||||
self._private_key = _get_private_key(private_key_pkcs8_text)
|
||||
self._private_key_pkcs8_text = private_key_pkcs8_text
|
||||
self._signer = signer
|
||||
self._scopes = util.scopes_to_string(scopes)
|
||||
self._private_key_id = private_key_id
|
||||
self.client_id = client_id
|
||||
self._user_agent = user_agent
|
||||
self._token_uri = token_uri
|
||||
self._revoke_uri = revoke_uri
|
||||
self._kwargs = kwargs
|
||||
|
||||
def _to_json(self, strip, to_serialize=None):
|
||||
"""Utility function that creates JSON repr. of a credentials object.
|
||||
|
||||
Over-ride is needed since PKCS#12 keys will not in general be JSON
|
||||
serializable.
|
||||
|
||||
Args:
|
||||
strip: array, An array of names of members to exclude from the
|
||||
JSON.
|
||||
to_serialize: dict, (Optional) The properties for this object
|
||||
that will be serialized. This allows callers to modify
|
||||
before serializing.
|
||||
|
||||
Returns:
|
||||
string, a JSON representation of this instance, suitable to pass to
|
||||
from_json().
|
||||
"""
|
||||
if to_serialize is None:
|
||||
to_serialize = copy.copy(self.__dict__)
|
||||
pkcs12_val = to_serialize.get(_PKCS12_KEY)
|
||||
if pkcs12_val is not None:
|
||||
to_serialize[_PKCS12_KEY] = base64.b64encode(pkcs12_val)
|
||||
return super(ServiceAccountCredentials, self)._to_json(
|
||||
strip, to_serialize=to_serialize)
|
||||
|
||||
@classmethod
|
||||
def _from_parsed_json_keyfile(cls, keyfile_dict, scopes):
|
||||
"""Helper for factory constructors from JSON keyfile.
|
||||
|
||||
Args:
|
||||
keyfile_dict: dict-like object, The parsed dictionary-like object
|
||||
containing the contents of the JSON keyfile.
|
||||
scopes: List or string, Scopes to use when acquiring an
|
||||
access token.
|
||||
|
||||
Returns:
|
||||
ServiceAccountCredentials, a credentials object created from
|
||||
the keyfile contents.
|
||||
|
||||
Raises:
|
||||
ValueError, if the credential type is not :data:`SERVICE_ACCOUNT`.
|
||||
KeyError, if one of the expected keys is not present in
|
||||
the keyfile.
|
||||
"""
|
||||
creds_type = keyfile_dict.get('type')
|
||||
if creds_type != SERVICE_ACCOUNT:
|
||||
raise ValueError('Unexpected credentials type', creds_type,
|
||||
'Expected', SERVICE_ACCOUNT)
|
||||
|
||||
service_account_email = keyfile_dict['client_email']
|
||||
private_key_pkcs8_pem = keyfile_dict['private_key']
|
||||
private_key_id = keyfile_dict['private_key_id']
|
||||
client_id = keyfile_dict['client_id']
|
||||
|
||||
signer = crypt.Signer.from_string(private_key_pkcs8_pem)
|
||||
credentials = cls(service_account_email, signer, scopes=scopes,
|
||||
private_key_id=private_key_id,
|
||||
client_id=client_id)
|
||||
credentials._private_key_pkcs8_pem = private_key_pkcs8_pem
|
||||
return credentials
|
||||
|
||||
@classmethod
|
||||
def from_json_keyfile_name(cls, filename, scopes=''):
|
||||
"""Factory constructor from JSON keyfile by name.
|
||||
|
||||
Args:
|
||||
filename: string, The location of the keyfile.
|
||||
scopes: List or string, (Optional) Scopes to use when acquiring an
|
||||
access token.
|
||||
|
||||
Returns:
|
||||
ServiceAccountCredentials, a credentials object created from
|
||||
the keyfile.
|
||||
|
||||
Raises:
|
||||
ValueError, if the credential type is not :data:`SERVICE_ACCOUNT`.
|
||||
KeyError, if one of the expected keys is not present in
|
||||
the keyfile.
|
||||
"""
|
||||
with open(filename, 'r') as file_obj:
|
||||
client_credentials = json.load(file_obj)
|
||||
return cls._from_parsed_json_keyfile(client_credentials, scopes)
|
||||
|
||||
@classmethod
|
||||
def from_json_keyfile_dict(cls, keyfile_dict, scopes=''):
|
||||
"""Factory constructor from parsed JSON keyfile.
|
||||
|
||||
Args:
|
||||
keyfile_dict: dict-like object, The parsed dictionary-like object
|
||||
containing the contents of the JSON keyfile.
|
||||
scopes: List or string, (Optional) Scopes to use when acquiring an
|
||||
access token.
|
||||
|
||||
Returns:
|
||||
ServiceAccountCredentials, a credentials object created from
|
||||
the keyfile.
|
||||
|
||||
Raises:
|
||||
ValueError, if the credential type is not :data:`SERVICE_ACCOUNT`.
|
||||
KeyError, if one of the expected keys is not present in
|
||||
the keyfile.
|
||||
"""
|
||||
return cls._from_parsed_json_keyfile(keyfile_dict, scopes)
|
||||
|
||||
@classmethod
|
||||
def _from_p12_keyfile_contents(cls, service_account_email,
|
||||
private_key_pkcs12,
|
||||
private_key_password=None, scopes=''):
|
||||
"""Factory constructor from JSON keyfile.
|
||||
|
||||
Args:
|
||||
service_account_email: string, The email associated with the
|
||||
service account.
|
||||
private_key_pkcs12: string, The contents of a PKCS#12 keyfile.
|
||||
private_key_password: string, (Optional) Password for PKCS#12
|
||||
private key. Defaults to ``notasecret``.
|
||||
scopes: List or string, (Optional) Scopes to use when acquiring an
|
||||
access token.
|
||||
|
||||
Returns:
|
||||
ServiceAccountCredentials, a credentials object created from
|
||||
the keyfile.
|
||||
|
||||
Raises:
|
||||
NotImplementedError if pyOpenSSL is not installed / not the
|
||||
active crypto library.
|
||||
"""
|
||||
if private_key_password is None:
|
||||
private_key_password = _PASSWORD_DEFAULT
|
||||
if crypt.Signer is not crypt.OpenSSLSigner:
|
||||
raise NotImplementedError(_PKCS12_ERROR)
|
||||
signer = crypt.Signer.from_string(private_key_pkcs12,
|
||||
private_key_password)
|
||||
credentials = cls(service_account_email, signer, scopes=scopes)
|
||||
credentials._private_key_pkcs12 = private_key_pkcs12
|
||||
credentials._private_key_password = private_key_password
|
||||
return credentials
|
||||
|
||||
@classmethod
|
||||
def from_p12_keyfile(cls, service_account_email, filename,
|
||||
private_key_password=None, scopes=''):
|
||||
"""Factory constructor from JSON keyfile.
|
||||
|
||||
Args:
|
||||
service_account_email: string, The email associated with the
|
||||
service account.
|
||||
filename: string, The location of the PKCS#12 keyfile.
|
||||
private_key_password: string, (Optional) Password for PKCS#12
|
||||
private key. Defaults to ``notasecret``.
|
||||
scopes: List or string, (Optional) Scopes to use when acquiring an
|
||||
access token.
|
||||
|
||||
Returns:
|
||||
ServiceAccountCredentials, a credentials object created from
|
||||
the keyfile.
|
||||
|
||||
Raises:
|
||||
NotImplementedError if pyOpenSSL is not installed / not the
|
||||
active crypto library.
|
||||
"""
|
||||
with open(filename, 'rb') as file_obj:
|
||||
private_key_pkcs12 = file_obj.read()
|
||||
return cls._from_p12_keyfile_contents(
|
||||
service_account_email, private_key_pkcs12,
|
||||
private_key_password=private_key_password, scopes=scopes)
|
||||
|
||||
@classmethod
|
||||
def from_p12_keyfile_buffer(cls, service_account_email, file_buffer,
|
||||
private_key_password=None, scopes=''):
|
||||
"""Factory constructor from JSON keyfile.
|
||||
|
||||
Args:
|
||||
service_account_email: string, The email associated with the
|
||||
service account.
|
||||
file_buffer: stream, A buffer that implements ``read()``
|
||||
and contains the PKCS#12 key contents.
|
||||
private_key_password: string, (Optional) Password for PKCS#12
|
||||
private key. Defaults to ``notasecret``.
|
||||
scopes: List or string, (Optional) Scopes to use when acquiring an
|
||||
access token.
|
||||
|
||||
Returns:
|
||||
ServiceAccountCredentials, a credentials object created from
|
||||
the keyfile.
|
||||
|
||||
Raises:
|
||||
NotImplementedError if pyOpenSSL is not installed / not the
|
||||
active crypto library.
|
||||
"""
|
||||
private_key_pkcs12 = file_buffer.read()
|
||||
return cls._from_p12_keyfile_contents(
|
||||
service_account_email, private_key_pkcs12,
|
||||
private_key_password=private_key_password, scopes=scopes)
|
||||
|
||||
def _generate_assertion(self):
|
||||
"""Generate the assertion that will be used in the request."""
|
||||
|
||||
header = {
|
||||
'alg': 'RS256',
|
||||
'typ': 'JWT',
|
||||
'kid': self._private_key_id
|
||||
}
|
||||
|
||||
now = int(time.time())
|
||||
payload = {
|
||||
'aud': self._token_uri,
|
||||
'aud': self.token_uri,
|
||||
'scope': self._scopes,
|
||||
'iat': now,
|
||||
'exp': now + _ServiceAccountCredentials.MAX_TOKEN_LIFETIME_SECS,
|
||||
'iss': self._service_account_email
|
||||
'exp': now + self.MAX_TOKEN_LIFETIME_SECS,
|
||||
'iss': self._service_account_email,
|
||||
}
|
||||
payload.update(self._kwargs)
|
||||
|
||||
first_segment = _urlsafe_b64encode(_json_encode(header))
|
||||
second_segment = _urlsafe_b64encode(_json_encode(payload))
|
||||
assertion_input = first_segment + b'.' + second_segment
|
||||
|
||||
# Sign the assertion.
|
||||
rsa_bytes = rsa.pkcs1.sign(assertion_input, self._private_key,
|
||||
'SHA-256')
|
||||
signature = base64.urlsafe_b64encode(rsa_bytes).rstrip(b'=')
|
||||
|
||||
return assertion_input + b'.' + signature
|
||||
return crypt.make_signed_jwt(self._signer, payload,
|
||||
key_id=self._private_key_id)
|
||||
|
||||
def sign_blob(self, blob):
|
||||
# Ensure that it is bytes
|
||||
blob = _to_bytes(blob, encoding='utf-8')
|
||||
return (self._private_key_id,
|
||||
rsa.pkcs1.sign(blob, self._private_key, 'SHA-256'))
|
||||
"""Cryptographically sign a blob (of bytes).
|
||||
|
||||
Implements abstract method
|
||||
:meth:`oauth2client.client.AssertionCredentials.sign_blob`.
|
||||
|
||||
Args:
|
||||
blob: bytes, Message to be signed.
|
||||
|
||||
Returns:
|
||||
tuple, A pair of the private key ID used to sign the blob and
|
||||
the signed contents.
|
||||
"""
|
||||
return self._private_key_id, self._signer.sign(blob)
|
||||
|
||||
@property
|
||||
def service_account_email(self):
|
||||
"""Get the email for the current service account.
|
||||
|
||||
Returns:
|
||||
string, The email associated with the service account.
|
||||
"""
|
||||
return self._service_account_email
|
||||
|
||||
@property
|
||||
def serialization_data(self):
|
||||
# NOTE: This is only useful for JSON keyfile.
|
||||
return {
|
||||
'type': 'service_account',
|
||||
'client_id': self._service_account_id,
|
||||
'client_email': self._service_account_email,
|
||||
'private_key_id': self._private_key_id,
|
||||
'private_key': self._private_key_pkcs8_text
|
||||
'private_key': self._private_key_pkcs8_pem,
|
||||
'client_id': self.client_id,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, json_data):
|
||||
"""Deserialize a JSON-serialized instance.
|
||||
|
||||
Inverse to :meth:`to_json`.
|
||||
|
||||
Args:
|
||||
json_data: dict or string, Serialized JSON (as a string or an
|
||||
already parsed dictionary) representing a credential.
|
||||
|
||||
Returns:
|
||||
ServiceAccountCredentials from the serialized data.
|
||||
"""
|
||||
if not isinstance(json_data, dict):
|
||||
json_data = json.loads(_from_bytes(json_data))
|
||||
|
||||
private_key_pkcs8_pem = None
|
||||
pkcs12_val = json_data.get(_PKCS12_KEY)
|
||||
password = None
|
||||
if pkcs12_val is None:
|
||||
private_key_pkcs8_pem = json_data['_private_key_pkcs8_pem']
|
||||
signer = crypt.Signer.from_string(private_key_pkcs8_pem)
|
||||
else:
|
||||
# NOTE: This assumes that private_key_pkcs8_pem is not also
|
||||
# in the serialized data. This would be very incorrect
|
||||
# state.
|
||||
pkcs12_val = base64.b64decode(pkcs12_val)
|
||||
password = json_data['_private_key_password']
|
||||
signer = crypt.Signer.from_string(pkcs12_val, password)
|
||||
|
||||
credentials = cls(
|
||||
json_data['_service_account_email'],
|
||||
signer,
|
||||
scopes=json_data['_scopes'],
|
||||
private_key_id=json_data['_private_key_id'],
|
||||
client_id=json_data['client_id'],
|
||||
user_agent=json_data['_user_agent'],
|
||||
**json_data['_kwargs']
|
||||
)
|
||||
if private_key_pkcs8_pem is not None:
|
||||
credentials._private_key_pkcs8_pem = private_key_pkcs8_pem
|
||||
if pkcs12_val is not None:
|
||||
credentials._private_key_pkcs12 = pkcs12_val
|
||||
if password is not None:
|
||||
credentials._private_key_password = password
|
||||
credentials.invalid = json_data['invalid']
|
||||
credentials.access_token = json_data['access_token']
|
||||
credentials.token_uri = json_data['token_uri']
|
||||
credentials.revoke_uri = json_data['revoke_uri']
|
||||
token_expiry = json_data.get('token_expiry', None)
|
||||
if token_expiry is not None:
|
||||
credentials.token_expiry = datetime.datetime.strptime(
|
||||
token_expiry, EXPIRY_FORMAT)
|
||||
return credentials
|
||||
|
||||
def create_scoped_required(self):
|
||||
return not self._scopes
|
||||
|
||||
def create_scoped(self, scopes):
|
||||
return _ServiceAccountCredentials(self._service_account_id,
|
||||
self._service_account_email,
|
||||
self._private_key_id,
|
||||
self._private_key_pkcs8_text,
|
||||
scopes,
|
||||
user_agent=self._user_agent,
|
||||
token_uri=self._token_uri,
|
||||
revoke_uri=self._revoke_uri,
|
||||
**self._kwargs)
|
||||
result = self.__class__(self._service_account_email,
|
||||
self._signer,
|
||||
scopes=scopes,
|
||||
private_key_id=self._private_key_id,
|
||||
client_id=self.client_id,
|
||||
user_agent=self._user_agent,
|
||||
**self._kwargs)
|
||||
result.token_uri = self.token_uri
|
||||
result.revoke_uri = self.revoke_uri
|
||||
result._private_key_pkcs8_pem = self._private_key_pkcs8_pem
|
||||
result._private_key_pkcs12 = self._private_key_pkcs12
|
||||
result._private_key_password = self._private_key_password
|
||||
return result
|
||||
|
||||
def create_delegated(self, sub):
|
||||
"""Create credentials that act as domain-wide delegation of authority.
|
||||
|
||||
def _get_private_key(private_key_pkcs8_text):
|
||||
"""Get an RSA private key object from a pkcs8 representation."""
|
||||
private_key_pkcs8_text = _to_bytes(private_key_pkcs8_text)
|
||||
der = rsa.pem.load_pem(private_key_pkcs8_text, 'PRIVATE KEY')
|
||||
asn1_private_key, _ = decoder.decode(der, asn1Spec=PrivateKeyInfo())
|
||||
return rsa.PrivateKey.load_pkcs1(
|
||||
asn1_private_key.getComponentByName('privateKey').asOctets(),
|
||||
format='DER')
|
||||
Use the ``sub`` parameter as the subject to delegate on behalf of
|
||||
that user.
|
||||
|
||||
For example::
|
||||
|
||||
>>> account_sub = 'foo@email.com'
|
||||
>>> delegate_creds = creds.create_delegated(account_sub)
|
||||
|
||||
Args:
|
||||
sub: string, An email address that this service account will
|
||||
act on behalf of (via domain-wide delegation).
|
||||
|
||||
Returns:
|
||||
ServiceAccountCredentials, a copy of the current service account
|
||||
updated to act on behalf of ``sub``.
|
||||
"""
|
||||
new_kwargs = dict(self._kwargs)
|
||||
new_kwargs['sub'] = sub
|
||||
result = self.__class__(self._service_account_email,
|
||||
self._signer,
|
||||
scopes=self._scopes,
|
||||
private_key_id=self._private_key_id,
|
||||
client_id=self.client_id,
|
||||
user_agent=self._user_agent,
|
||||
**new_kwargs)
|
||||
result.token_uri = self.token_uri
|
||||
result.revoke_uri = self.revoke_uri
|
||||
result._private_key_pkcs8_pem = self._private_key_pkcs8_pem
|
||||
result._private_key_pkcs12 = self._private_key_pkcs12
|
||||
result._private_key_password = self._private_key_password
|
||||
return result
|
||||
|
@ -26,6 +26,7 @@ import socket
|
||||
import sys
|
||||
|
||||
from six.moves import BaseHTTPServer
|
||||
from six.moves import http_client
|
||||
from six.moves import urllib
|
||||
from six.moves import input
|
||||
|
||||
@ -95,7 +96,7 @@ class ClientRedirectHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
||||
if the flow has completed. Note that we can't detect
|
||||
if an error occurred.
|
||||
"""
|
||||
self.send_response(200)
|
||||
self.send_response(http_client.OK)
|
||||
self.send_header("Content-type", "text/html")
|
||||
self.end_headers()
|
||||
query = self.path.split('?', 1)[-1]
|
||||
@ -112,7 +113,7 @@ class ClientRedirectHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
||||
|
||||
|
||||
@util.positional(3)
|
||||
def run_flow(flow, storage, flags, http=None):
|
||||
def run_flow(flow, storage, flags=None, http=None):
|
||||
"""Core code for a command-line application.
|
||||
|
||||
The ``run()`` function is called from your application and runs
|
||||
@ -153,15 +154,18 @@ def run_flow(flow, storage, flags, http=None):
|
||||
Args:
|
||||
flow: Flow, an OAuth 2.0 Flow to step through.
|
||||
storage: Storage, a ``Storage`` to store the credential in.
|
||||
flags: ``argparse.Namespace``, The command-line flags. This is the
|
||||
object returned from calling ``parse_args()`` on
|
||||
``argparse.ArgumentParser`` as described above.
|
||||
flags: ``argparse.Namespace``, (Optional) The command-line flags. This
|
||||
is the object returned from calling ``parse_args()`` on
|
||||
``argparse.ArgumentParser`` as described above. Defaults
|
||||
to ``argparser.parse_args()``.
|
||||
http: An instance of ``httplib2.Http.request`` or something that
|
||||
acts like it.
|
||||
|
||||
Returns:
|
||||
Credentials, the obtained credential.
|
||||
"""
|
||||
if flags is None:
|
||||
flags = argparser.parse_args()
|
||||
logging.getLogger().setLevel(getattr(logging, flags.logging_level))
|
||||
if not flags.noauth_local_webserver:
|
||||
success = False
|
||||
@ -211,8 +215,11 @@ def run_flow(flow, storage, flags, http=None):
|
||||
print()
|
||||
print(' ' + authorize_url)
|
||||
print()
|
||||
print('If your browser is on a different machine then exit and re-run this')
|
||||
print('after creating a file called nobrowser.txt in the same path as GAM.')
|
||||
print('If your browser is on a different machine then '
|
||||
'exit and re-run this')
|
||||
print('application with the command-line parameter ')
|
||||
print()
|
||||
print(' --noauth_local_webserver')
|
||||
print()
|
||||
else:
|
||||
print('Go to the following link in your browser:')
|
||||
|
@ -1,5 +1,3 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright 2014 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@ -13,15 +11,12 @@
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
"""Common utility library."""
|
||||
|
||||
import functools
|
||||
import inspect
|
||||
import logging
|
||||
import sys
|
||||
import types
|
||||
|
||||
import six
|
||||
from six.moves import urllib
|
||||
@ -188,21 +183,6 @@ def string_to_scopes(scopes):
|
||||
return scopes
|
||||
|
||||
|
||||
def dict_to_tuple_key(dictionary):
|
||||
"""Converts a dictionary to a tuple that can be used as an immutable key.
|
||||
|
||||
The resulting key is always sorted so that logically equivalent
|
||||
dictionaries always produce an identical tuple for a key.
|
||||
|
||||
Args:
|
||||
dictionary: the dictionary to use as the key.
|
||||
|
||||
Returns:
|
||||
A tuple representing the dictionary in it's naturally sorted ordering.
|
||||
"""
|
||||
return tuple(sorted(dictionary.items()))
|
||||
|
||||
|
||||
def _add_query_parameter(url, name, value):
|
||||
"""Adds a query parameter to a url.
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user