Add OAuth2 support for calcurse-caldav
OAuth2 authentication is completely optional. It is controlled by the AuthMethod option in the config file. Other required options were appended to config.sample. Signed-off-by: Randy Ramos <rramos1295@gmail.com> Signed-off-by: Lukas Fleischer <lfleischer@calcurse.org>
This commit is contained in:
parent
1e1d61585d
commit
479e39fbb7
@ -12,6 +12,14 @@ import textwrap
|
|||||||
import urllib.parse
|
import urllib.parse
|
||||||
import xml.etree.ElementTree as etree
|
import xml.etree.ElementTree as etree
|
||||||
|
|
||||||
|
# Optional libraries for OAuth2 authentication
|
||||||
|
try:
|
||||||
|
from oauth2client.client import OAuth2WebServerFlow, HttpAccessTokenRefreshError
|
||||||
|
from oauth2client.file import Storage
|
||||||
|
import webbrowser
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def msgfmt(msg, prefix=''):
|
def msgfmt(msg, prefix=''):
|
||||||
lines = []
|
lines = []
|
||||||
@ -92,6 +100,65 @@ def get_auth_headers():
|
|||||||
return headers
|
return headers
|
||||||
|
|
||||||
|
|
||||||
|
def init_auth(client_id, client_secret, scope, redirect_uri, authcode):
|
||||||
|
# Create OAuth2 session
|
||||||
|
oauth2_client = OAuth2WebServerFlow(client_id=client_id,
|
||||||
|
client_secret=client_secret,
|
||||||
|
scope=scope,
|
||||||
|
redirect_uri=redirect_uri)
|
||||||
|
|
||||||
|
# If auth code is missing, tell user run script with auth code
|
||||||
|
if not authcode:
|
||||||
|
# Generate and open URL for user to authorize
|
||||||
|
auth_uri = oauth2_client.step1_get_authorize_url()
|
||||||
|
webbrowser.open(auth_uri)
|
||||||
|
|
||||||
|
prompt = ('\nIf a browser window did not open, go to the URL '
|
||||||
|
'below and log in to authorize syncing. '
|
||||||
|
'Once authorized, pass the string after "code=" from '
|
||||||
|
'the URL in your browser\'s address bar to '
|
||||||
|
'calcurse-caldav.py using the "--authcode" flag. '
|
||||||
|
"Example: calcurse-caldav --authcode "
|
||||||
|
"'your_auth_code_here'\n\n{}\n".format(auth_uri))
|
||||||
|
print(prompt)
|
||||||
|
die("Access token is missing or refresh token is expired.")
|
||||||
|
|
||||||
|
# Create and return Credential object from auth code
|
||||||
|
credentials = oauth2_client.step2_exchange(authcode)
|
||||||
|
|
||||||
|
# Setup storage file and store credentials
|
||||||
|
storage = Storage(oauth_file)
|
||||||
|
credentials.set_store(storage)
|
||||||
|
storage.put(credentials)
|
||||||
|
|
||||||
|
return credentials
|
||||||
|
|
||||||
|
|
||||||
|
def run_auth(authcode):
|
||||||
|
# Check if credentials file exists
|
||||||
|
if os.path.isfile(oauth_file):
|
||||||
|
|
||||||
|
# Retrieve token from file
|
||||||
|
storage = Storage(oauth_file)
|
||||||
|
credentials = storage.get()
|
||||||
|
|
||||||
|
# Set file to store it in for future functions
|
||||||
|
credentials.set_store(storage)
|
||||||
|
|
||||||
|
# Refresh the access token if it is expired
|
||||||
|
if credentials.invalid:
|
||||||
|
try:
|
||||||
|
credentials.refresh(httplib2.Http())
|
||||||
|
except HttpAccessTokenRefreshError:
|
||||||
|
# Initialize OAuth2 again if refresh token becomes invalid
|
||||||
|
credentials = init_auth(client_id, client_secret, scope, redirect_uri, authcode)
|
||||||
|
else:
|
||||||
|
# Initialize OAuth2 credentials
|
||||||
|
credentials = init_auth(client_id, client_secret, scope, redirect_uri, authcode)
|
||||||
|
|
||||||
|
return credentials
|
||||||
|
|
||||||
|
|
||||||
def remote_query(conn, cmd, path, additional_headers, body):
|
def remote_query(conn, cmd, path, additional_headers, body):
|
||||||
headers = custom_headers.copy()
|
headers = custom_headers.copy()
|
||||||
headers.update(get_auth_headers())
|
headers.update(get_auth_headers())
|
||||||
@ -384,6 +451,7 @@ configfn = os.path.expanduser("~/.calcurse/caldav/config")
|
|||||||
lockfn = os.path.expanduser("~/.calcurse/caldav/lock")
|
lockfn = os.path.expanduser("~/.calcurse/caldav/lock")
|
||||||
syncdbfn = os.path.expanduser("~/.calcurse/caldav/sync.db")
|
syncdbfn = os.path.expanduser("~/.calcurse/caldav/sync.db")
|
||||||
hookdir = os.path.expanduser("~/.calcurse/caldav/hooks/")
|
hookdir = os.path.expanduser("~/.calcurse/caldav/hooks/")
|
||||||
|
oauth_file = os.path.expanduser("~/.calcurse/caldav/oauth2_cred")
|
||||||
|
|
||||||
# Parse command line arguments.
|
# Parse command line arguments.
|
||||||
parser = argparse.ArgumentParser('calcurse-caldav')
|
parser = argparse.ArgumentParser('calcurse-caldav')
|
||||||
@ -402,6 +470,9 @@ parser.add_argument('--syncdb', action='store', dest='syncdbfn',
|
|||||||
parser.add_argument('--hookdir', action='store', dest='hookdir',
|
parser.add_argument('--hookdir', action='store', dest='hookdir',
|
||||||
default=hookdir,
|
default=hookdir,
|
||||||
help='path to the calcurse-caldav hooks directory')
|
help='path to the calcurse-caldav hooks directory')
|
||||||
|
parser.add_argument('--authcode', action='store', dest='authcode',
|
||||||
|
default=None,
|
||||||
|
help='auth code for OAuth2 authentication')
|
||||||
parser.add_argument('-v', '--verbose', action='store_true', dest='verbose',
|
parser.add_argument('-v', '--verbose', action='store_true', dest='verbose',
|
||||||
default=False,
|
default=False,
|
||||||
help='print status messages to stdout')
|
help='print status messages to stdout')
|
||||||
@ -414,6 +485,7 @@ configfn = args.configfn
|
|||||||
lockfn = args.lockfn
|
lockfn = args.lockfn
|
||||||
syncdbfn = args.syncdbfn
|
syncdbfn = args.syncdbfn
|
||||||
hookdir = args.hookdir
|
hookdir = args.hookdir
|
||||||
|
authcode = args.authcode
|
||||||
verbose = args.verbose
|
verbose = args.verbose
|
||||||
debug = args.debug
|
debug = args.debug
|
||||||
|
|
||||||
@ -452,6 +524,11 @@ if not verbose and config.has_option('General', 'Verbose'):
|
|||||||
if not debug and config.has_option('General', 'Debug'):
|
if not debug and config.has_option('General', 'Debug'):
|
||||||
debug = config.getboolean('General', 'Debug')
|
debug = config.getboolean('General', 'Debug')
|
||||||
|
|
||||||
|
if config.has_option('General', 'AuthMethod'):
|
||||||
|
authmethod = config.get('General', 'AuthMethod').lower()
|
||||||
|
else:
|
||||||
|
authmethod = 'basic'
|
||||||
|
|
||||||
if config.has_option('Auth', 'UserName'):
|
if config.has_option('Auth', 'UserName'):
|
||||||
username = config.get('Auth', 'UserName')
|
username = config.get('Auth', 'UserName')
|
||||||
else:
|
else:
|
||||||
@ -467,6 +544,26 @@ if config.has_section('CustomHeaders'):
|
|||||||
else:
|
else:
|
||||||
custom_headers = {}
|
custom_headers = {}
|
||||||
|
|
||||||
|
if config.has_option('OAuth2', 'ClientID'):
|
||||||
|
client_id = config.get('OAuth2', 'ClientID')
|
||||||
|
else:
|
||||||
|
client_id = None
|
||||||
|
|
||||||
|
if config.has_option('OAuth2', 'ClientSecret'):
|
||||||
|
client_secret = config.get('OAuth2', 'ClientSecret')
|
||||||
|
else:
|
||||||
|
client_secret = None
|
||||||
|
|
||||||
|
if config.has_option('OAuth2', 'Scope'):
|
||||||
|
scope = config.get('OAuth2', 'Scope')
|
||||||
|
else:
|
||||||
|
scope = None
|
||||||
|
|
||||||
|
if config.has_option('OAuth2', 'RedirectURI'):
|
||||||
|
redirect_uri = config.get('OAuth2', 'RedirectURI')
|
||||||
|
else:
|
||||||
|
redirect_uri = 'http://127.0.0.1'
|
||||||
|
|
||||||
# Show disclaimer when performing a dry run.
|
# Show disclaimer when performing a dry run.
|
||||||
if dry_run:
|
if dry_run:
|
||||||
warn(('Dry run; nothing is imported/exported. Add "DryRun = No" to the '
|
warn(('Dry run; nothing is imported/exported. Add "DryRun = No" to the '
|
||||||
@ -500,6 +597,13 @@ try:
|
|||||||
if insecure_ssl:
|
if insecure_ssl:
|
||||||
conn.disable_ssl_certificate_validation = True
|
conn.disable_ssl_certificate_validation = True
|
||||||
|
|
||||||
|
if authmethod == 'oauth2':
|
||||||
|
# Authenticate with OAuth2 and authorize HTTP object
|
||||||
|
cred = run_auth(authcode)
|
||||||
|
conn = cred.authorize(conn)
|
||||||
|
elif authmethod != 'basic':
|
||||||
|
die('Invalid option for AuthMethod in config file. Use "basic" or "oauth2"')
|
||||||
|
|
||||||
if init:
|
if init:
|
||||||
# In initialization mode, start with an empty synchronization database.
|
# In initialization mode, start with an empty synchronization database.
|
||||||
if args.init == 'keep-remote':
|
if args.init == 'keep-remote':
|
||||||
@ -551,6 +655,10 @@ try:
|
|||||||
# Write the synchronization database.
|
# Write the synchronization database.
|
||||||
save_syncdb(syncdbfn, syncdb)
|
save_syncdb(syncdbfn, syncdb)
|
||||||
|
|
||||||
|
#Clear OAuth2 credentials if used
|
||||||
|
if authmethod == 'oauth2':
|
||||||
|
conn.clear_credentials()
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
# Remove lock file.
|
# Remove lock file.
|
||||||
os.remove(lockfn)
|
os.remove(lockfn)
|
||||||
|
@ -12,6 +12,9 @@ Hostname = some.hostname.com
|
|||||||
# Path to the CalDAV calendar on the host specified above.
|
# Path to the CalDAV calendar on the host specified above.
|
||||||
Path = /path/to/calendar/on/the/server/
|
Path = /path/to/calendar/on/the/server/
|
||||||
|
|
||||||
|
# Type of authentication to use. Must be "basic" or "oauth2"
|
||||||
|
#AuthMethod = basic
|
||||||
|
|
||||||
# Enable this if you want to skip SSL certificate checks.
|
# Enable this if you want to skip SSL certificate checks.
|
||||||
InsecureSSL = No
|
InsecureSSL = No
|
||||||
|
|
||||||
@ -33,3 +36,16 @@ Verbose = Yes
|
|||||||
# Optionally specify additional HTTP headers here.
|
# Optionally specify additional HTTP headers here.
|
||||||
#[CustomHeaders]
|
#[CustomHeaders]
|
||||||
#User-Agent = Mac_OS_X/10.9.2 (13C64) CalendarAgent/176
|
#User-Agent = Mac_OS_X/10.9.2 (13C64) CalendarAgent/176
|
||||||
|
|
||||||
|
# Use the following to synchronize with an OAuth2-based service
|
||||||
|
# such as Google Calendar.
|
||||||
|
#[OAuth2]
|
||||||
|
#ClientID = your_client_id
|
||||||
|
#ClientSecret = your_client_secret
|
||||||
|
|
||||||
|
# Scope of access for API calls. Synchronization requires read/write.
|
||||||
|
#Scope = https://example.com/resource/scope
|
||||||
|
|
||||||
|
# Change the redirect URI if you receive errors, but ensure that it is identical
|
||||||
|
# to the redirect URI you specified in the API settings.
|
||||||
|
#RedirectURI = http://127.0.0.1
|
||||||
|
Loading…
x
Reference in New Issue
Block a user