Skip to content

Instantly share code, notes, and snippets.

@gslin
Created April 25, 2023 19:50
Show Gist options
  • Save gslin/5e36a177ccbb0cc5af63989fa2b8ce52 to your computer and use it in GitHub Desktop.
Save gslin/5e36a177ccbb0cc5af63989fa2b8ce52 to your computer and use it in GitHub Desktop.
from six.moves.urllib.parse import parse_qs, quote
from trac.core import Component, implements
from trac.util import hex_entropy
from trac.util.datefmt import time_now
from trac.util.html import tag
from trac.util.translation import _, tag_
from trac.web import IAuthenticator, IRequestHandler
from trac.web.chrome import Chrome, INavigationContributor
import re
import requests
class TracGoogleOAuthIntegration(Component):
implements(INavigationContributor, IRequestHandler)
def __init__(self):
super(TracGoogleOAuthIntegration, self).__init__()
self.client_id = self.config.get('tracgoogleoauthintegration', 'client_id')
self.client_secret = self.config.get('tracgoogleoauthintegration', 'client_secret')
self.domain_regex = self.config.get('tracgoogleoauthintegration', 'domain_regex')
self.base_url = self.config.get('trac', 'base_url')
self.redirect_uri = self.base_url + '/googleoauth/redirect_uri'
# INavigationContributor methods
def get_active_navigation_item(self, req):
return 'login'
def get_navigation_items(self, req):
if req.is_authenticated:
yield ('metanav', 'login',
tag_("logged in as %(user)s",
user=Chrome(self.env).authorinfo(req, req.authname)))
yield ('metanav', 'logout',
tag.form(
tag.div(
tag.button(_("Logout"), name='logout',
type='submit'),
tag.input(type='hidden', name='__FORM_TOKEN',
value=req.form_token)
),
action=req.href.logout(), method='post',
id='logout', class_='trac-logout'))
else:
yield ('metanav', 'login',
tag.a(_("Login"), href='/googleoauth/login'))
# IRequestHandler methods
def match_request(self, req):
if req.path_info == '/googleoauth/login':
return True
if req.path_info == '/googleoauth/redirect_uri':
return True
def process_request(self, req):
if self.base_url == None:
req.send('trac base_url is not set', 'text/plain')
return
if req.path_info == '/googleoauth/login':
# Follow server-side web apps:
# https://developers.google.com/identity/protocols/oauth2/web-server
url = 'https://accounts.google.com/o/oauth2/v2/auth?' + \
'client_id={}'.format(quote(self.client_id)) + \
'&redirect_uri={}'.format(quote(self.redirect_uri)) + \
'&response_type=code' + \
'&scope=profile+email'
req.redirect(url)
if req.path_info == '/googleoauth/redirect_uri':
# Get access_token
url = 'https://oauth2.googleapis.com/token'
qsdata = parse_qs(req.query_string)
code = qsdata['code']
data = {
'code': code,
'client_id': self.client_id,
'client_secret': self.client_secret,
'redirect_uri': self.redirect_uri,
'grant_type': 'authorization_code',
}
res = requests.post(url, data=data)
access_token = res.json()['access_token']
# Get userinfo
url = 'https://www.googleapis.com/oauth2/v3/userinfo'
res = requests.get(url, headers={'Authorization': 'Bearer ' + access_token})
resdata = res.json()
if resdata['email_verified'] != True:
req.send('email_verified is not True', 'text/plain')
return
email = resdata['email']
regex = '^(\\S+)@(' + self.domain_regex + ')$'
obj = re.search(regex, email)
if obj is None:
req.send('email and domain_regex are not matched', 'text/plain')
return
username = obj.group(1)
# XXX: reuse LoginModule's cookie format (side effect).
cookie = hex_entropy()
with self.env.db_transaction as db:
db("""
INSERT INTO auth_cookie (cookie, name, ipnr, time)
VALUES (%s, %s, %s, %s)
""", (cookie, username, req.remote_addr, int(time_now())))
req.authname = username
req.outcookie['trac_auth'] = cookie
req.outcookie['trac_auth']['expires'] = 31536000
req.outcookie['trac_auth']['httponly'] = True
req.outcookie['trac_auth']['path'] = '/'
req.outcookie['trac_auth']['secure'] = True
req.redirect(self.base_url)
[tracgoogleoauthintegration]
client_id = x.apps.googleusercontent.com
client_secret = x
domain_regex = .*
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment