I'm trying to create a Django admin action that will create a draft email in my Gmail account, addressed to selected contacts. I'm getting stuck with the Google OAuth flow.
admin.py:
...
DEBUG = os.getenv('DEBUG', 'False') == 'True'
if DEBUG:
os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'
SCOPES = ['https://www.googleapis.com/auth/gmail.compose']
def email_contacts(modeladmin, request, queryset):
flow = Flow.from_client_secrets_file(
'contacts/client_secret.json',
scopes=SCOPES)
flow.redirect_uri = "http://localhost:8000/callback"
authorization_url, state = flow.authorization_url(
access_type='offline',
include_granted_scopes='true')
return HttpResponseRedirect(authorization_url)
def auth_callback(request):
code = request.GET.get('code')
flow = Flow.from_client_secrets_file(
'contacts/client_secret.json',
scopes=SCOPES)
flow.redirect_uri = "http://localhost:8000"
flow.fetch_token(code=code)
creds = flow.credentials
send_email(creds)
def send_email(creds):
message_body = "Test content"
message = MIMEMultipart()
message['to'] = '[email protected]'
message.attach(MIMEText(message_body, "plain"))
try:
service = build('gmail', 'v1', credentials=creds)
message = {'message': {'raw': base64.urlsafe_b64encode(message.as_bytes()).decode()}}
service.users().drafts().create(userId='me', body=message).execute()
except HttpError as err:
print(err)
...
class ContactAdmin(admin.ModelAdmin):
actions = [emails_contacts]
(Just trying to draft a test email so far; not yet trying to populate the email with data from the queryset)
urls.py:
...
from contacts.admin import auth_callback
urlpatterns = [
path('callback/', auth_callback, name='oauth_callback'),
path('admin/', admin.site.urls),
...
client_secret.json:
{"web":{"client_id":"....apps.googleusercontent.com","project_id":"...","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","...":"https://www.googleapis.com/oauth2/v1/certs","client_secret":"...","redirect_uris":["http://localhost:8000/callback","http://localhost:8000/callback/","http://localhost/callback","http://localhost/callback/","http://localhost:8000/","http://localhost:8000","http://localhost","http://localhost/"]}}
(Listing lots of redirect_uris to be safe)
The error:
CustomOAuth2Error at /callback/
(redirect_uri_mismatch) Bad Request
Request Method: GET Request URL: http://localhost:8000/callback/?state=...&code=...&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fgmail.compose Django Version: 4.2.1 Exception Type: CustomOAuth2Error Exception Value:
(redirect_uri_mismatch) Bad Request
Exception Location: /home/me/.local/share/virtualenvs/contacts/lib/python3.9/site-packages/oauthlib/oauth2/rfc6749/errors.py, line 400, in raise_from_error Raised during: contacts.admin.auth_callback Python Executable: /home/me/.local/share/virtualenvs/contacts/bin/python Python Version: 3.9.5
...
The error is triggering on this line of code: flow.fetch_token(code=code)
I got it working. I don't know exactly what the problem was but here are the relevant parts of the working code.
admin.py:
...
DEBUG = os.getenv('DEBUG', 'False') == 'True'
if DEBUG:
redirect_uri='https://127.0.0.1:8000/oauth/'
else:
redirect_uri='https://my-url.com/oauth/'
SCOPES = ['https://www.googleapis.com/auth/gmail.compose']
def authenticate(request):
flow = Flow.from_client_secrets_file('contacts/client_secret.json',
scopes=SCOPES,
redirect_uri=redirect_uri,
)
authorization_url, state = flow.authorization_url(
access_type='offline',
include_granted_scopes='true'
)
request.session['oauth2_state'] = state
return redirect(authorization_url)
def oauth2callback(request):
state = request.session.get('oauth2_state', '')
flow = Flow.from_client_secrets_file('contacts/client_secret.json',
scopes=SCOPES,
state=state,
redirect_uri=request.build_absolute_uri('/oauth/')
)
flow.fetch_token(authorization_response=request.build_absolute_uri())
credentials_json = flow.credentials.to_json()
request.session['gmail_credentials'] = credentials_json
messages.success(request, 'OAuth authentication successful but no emails drafted. Please perform action again.')
return redirect(request.build_absolute_uri('/admin/contacts/contact/'))
def get_gmail_credentials(request):
credentials_json = request.session.get('gmail_credentials')
if not credentials_json:
return None
credentials_info = json.loads(credentials_json)
info = {
"client_id": credentials_info['client_id'],
"client_secret": credentials_info['client_secret'],
"token": credentials_info['token'],
"refresh_token": credentials_info['refresh_token'],
"token_uri": credentials_info['token_uri'],
"scopes": SCOPES,
}
return credentials.Credentials.from_authorized_user_info(info)
...
class ContactAdmin(admin.ModelAdmin):
actions = [emails_contacts]
urls.py:
...
urlpatterns = [
path('authenticate/', authenticate, name='authenticate'),
path('oauth/', oauth2callback, name='oauth'),
path('compose-email/', compose_emails, name='compose_email'),
...
client_secret.json:
{"web":{"client_id":"MASKED.apps.googleusercontent.com","project_id":"MASKED","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":"MASKED","redirect_uris":["https://127.0.0.1:8000/oauth/","https://my-url.com/oauth/"]}}