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 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=''):
|
||||
lines = []
|
||||
@ -92,6 +100,65 @@ def get_auth_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):
|
||||
headers = custom_headers.copy()
|
||||
headers.update(get_auth_headers())
|
||||
@ -384,6 +451,7 @@ configfn = os.path.expanduser("~/.calcurse/caldav/config")
|
||||
lockfn = os.path.expanduser("~/.calcurse/caldav/lock")
|
||||
syncdbfn = os.path.expanduser("~/.calcurse/caldav/sync.db")
|
||||
hookdir = os.path.expanduser("~/.calcurse/caldav/hooks/")
|
||||
oauth_file = os.path.expanduser("~/.calcurse/caldav/oauth2_cred")
|
||||
|
||||
# Parse command line arguments.
|
||||
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',
|
||||
default=hookdir,
|
||||
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',
|
||||
default=False,
|
||||
help='print status messages to stdout')
|
||||
@ -414,6 +485,7 @@ configfn = args.configfn
|
||||
lockfn = args.lockfn
|
||||
syncdbfn = args.syncdbfn
|
||||
hookdir = args.hookdir
|
||||
authcode = args.authcode
|
||||
verbose = args.verbose
|
||||
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'):
|
||||
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'):
|
||||
username = config.get('Auth', 'UserName')
|
||||
else:
|
||||
@ -467,6 +544,26 @@ if config.has_section('CustomHeaders'):
|
||||
else:
|
||||
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.
|
||||
if dry_run:
|
||||
warn(('Dry run; nothing is imported/exported. Add "DryRun = No" to the '
|
||||
@ -500,6 +597,13 @@ try:
|
||||
if insecure_ssl:
|
||||
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:
|
||||
# In initialization mode, start with an empty synchronization database.
|
||||
if args.init == 'keep-remote':
|
||||
@ -551,6 +655,10 @@ try:
|
||||
# Write the synchronization database.
|
||||
save_syncdb(syncdbfn, syncdb)
|
||||
|
||||
#Clear OAuth2 credentials if used
|
||||
if authmethod == 'oauth2':
|
||||
conn.clear_credentials()
|
||||
|
||||
finally:
|
||||
# Remove lock file.
|
||||
os.remove(lockfn)
|
||||
|
@ -12,6 +12,9 @@ Hostname = some.hostname.com
|
||||
# Path to the CalDAV calendar on the host specified above.
|
||||
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.
|
||||
InsecureSSL = No
|
||||
|
||||
@ -33,3 +36,16 @@ Verbose = Yes
|
||||
# Optionally specify additional HTTP headers here.
|
||||
#[CustomHeaders]
|
||||
#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