Skip to content

Instantly share code, notes, and snippets.

@smugmug-api-docs
Forked from chestone/common.py
Last active March 6, 2023 06:10
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save smugmug-api-docs/10046914 to your computer and use it in GitHub Desktop.
Save smugmug-api-docs/10046914 to your computer and use it in GitHub Desktop.
OAuth 1.0a Example Code for the SmugMug API
ENV
__pycache__
*.json
!example.json

SmugMug OAuth Example Code

This repository contains two example programs written in Python 3 which demonstrate the use of OAuth with SmugMug.

The programs are:

  • web.py, which demonstrates OAuth as used by a web application
  • console.py, which demonstrates how to use OAuth in non-web-application scenarios

Before running either example, you must supply a SmugMug API Key and Secret in a file named config.json. The expected format of this file is demonstrated by example.json.

The Web Example

To run the web example, you can just run ./run-web.sh. This shell script will install the Python libraries needed by the example and then run it.

The Non-Web Example

To run the non-web example, you can just run ./run-console.sh. This shell script will install the Python libraries needed by the example and then run it.

import json
from rauth import OAuth1Service
import sys
from urllib.parse import urlsplit, urlunsplit, parse_qsl, urlencode
OAUTH_ORIGIN = 'https://secure.smugmug.com'
REQUEST_TOKEN_URL = OAUTH_ORIGIN + '/services/oauth/1.0a/getRequestToken'
ACCESS_TOKEN_URL = OAUTH_ORIGIN + '/services/oauth/1.0a/getAccessToken'
AUTHORIZE_URL = OAUTH_ORIGIN + '/services/oauth/1.0a/authorize'
API_ORIGIN = 'https://api.smugmug.com'
SERVICE = None
def get_service():
global SERVICE
if SERVICE is None:
try:
with open('config.json', 'r') as fh:
config = json.load(fh)
except IOError as e:
print('====================================================')
print('Failed to open config.json! Did you create it?')
print('The expected format is demonstrated in example.json.')
print('====================================================')
sys.exit(1)
if type(config) is not dict \
or 'key' not in config \
or 'secret' not in config\
or type(config['key']) is not str \
or type(config['secret']) is not str:
print('====================================================')
print('Invalid config.json!')
print('The expected format is demonstrated in example.json.')
print('====================================================')
sys.exit(1)
SERVICE = OAuth1Service(
name='smugmug-oauth-web-demo',
consumer_key=config['key'],
consumer_secret=config['secret'],
request_token_url=REQUEST_TOKEN_URL,
access_token_url=ACCESS_TOKEN_URL,
authorize_url=AUTHORIZE_URL,
base_url=API_ORIGIN + '/api/v2')
return SERVICE
def add_auth_params(auth_url, access=None, permissions=None):
if access is None and permissions is None:
return auth_url
parts = urlsplit(auth_url)
query = parse_qsl(parts.query, True)
if access is not None:
query.append(('Access', access))
if permissions is not None:
query.append(('Permissions', permissions))
return urlunsplit((
parts.scheme,
parts.netloc,
parts.path,
urlencode(query, True),
parts.fragment))
#!/usr/bin/env python3
from rauth import OAuth1Session
import sys
from common import API_ORIGIN, get_service, add_auth_params
def main():
"""This example interacts with its user through the console, but it is
similar in principle to the way any non-web-based application can obtain an
OAuth authorization from a user."""
service = get_service()
# First, we need a request token and secret, which SmugMug will give us.
# We are specifying "oob" (out-of-band) as the callback because we don't
# have a website for SmugMug to call back to.
rt, rts = service.get_request_token(params={'oauth_callback': 'oob'})
# Second, we need to give the user the web URL where they can authorize our
# application.
auth_url = add_auth_params(
service.get_authorize_url(rt), access='Full', permissions='Modify')
print('Go to %s in a web browser.' % auth_url)
# Once the user has authorized our application, they will be given a
# six-digit verifier code. Our third step is to ask the user to enter that
# code:
sys.stdout.write('Enter the six-digit code: ')
sys.stdout.flush()
verifier = sys.stdin.readline().strip()
# Finally, we can use the verifier code, along with the request token and
# secret, to sign a request for an access token.
at, ats = service.get_access_token(rt, rts, params={'oauth_verifier': verifier})
# The access token we have received is valid forever, unless the user
# revokes it. Let's make one example API request to show that the access
# token works.
print('Access token: %s' % at)
print('Access token secret: %s' % ats)
session = OAuth1Session(
service.consumer_key,
service.consumer_secret,
access_token=at,
access_token_secret=ats)
print(session.get(
API_ORIGIN + '/api/v2!authuser',
headers={'Accept': 'application/json'}).text)
if __name__ == '__main__':
main()
{
"key": "Put your API Key here",
"secret": "Put your API Key Secret here"
}
#!/bin/sh
if [ ! -d ENV ]; then
pyvenv ENV
fi
. ENV/bin/activate
pip install -r requirements.txt
Copyright (c) 2014 SmugMug, Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
THIS SOFTWARE IS PROVIDED BY SMUGMUG, INC. ``AS IS'' AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL SMUGMUG, INC. BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
GOODS OR SERVICES;LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
rauth==0.7.1
bottle==0.12.8
#!/bin/sh
. init.sh
exec python console.py
#!/bin/sh
. init.sh
exec python web.py
#!/usr/bin/env python3
from bottle import redirect, request, response, route, run
import html
from rauth import OAuth1Session
from common import get_service, add_auth_params
from webutil import *
HOST = 'localhost'
PORT = 8090
@route('/')
def index():
"""This route is the main page."""
response.set_header('Content-Type', 'text/html')
if cookie_has_access_token():
# We already have an access token for this client, so show them the
# form where they can input an API path to request.
return make_html_page(TEST_FORM)
else:
# We don't have an access token for this client, so give them a link to
# begin the authorization process.
return make_html_page('<a href="/authorize">Authorize</a>');
@route('/authorize')
def authorize():
"""This route is where our client goes to begin the authorization
process."""
service = get_service()
# Get a request token and secret from SmugMug. This token enables us to
# make an authorization request.
rt, rts = service.get_request_token(
params={'oauth_callback': 'http://localhost:8090/callback'})
# Record the request token and secret in the client's cookie, because we're
# going to need it later.
set_cookie({'rt': rt, 'rts': rts})
# Get the authorization URL, which is where we send the user so they can
# approve our authorization request.
auth_url = service.get_authorize_url(rt)
# Modify the authorization URL to include the access and permissions levels
# that our application needs.
auth_url = add_auth_params(auth_url, access='Full', permissions='Modify')
# Send the client to the authorization URL.
redirect(auth_url)
@route('/callback')
def callback():
"""This route is where we receive the callback after the user accepts or
rejects the authorization request."""
service = get_service()
# Get the client's cookie, which contains the request token and secret that
# we saved earlier.
cookie = request.get_cookie(COOKIE_NAME, secret=SECRET)
# Use the request token and secret, and the verifier code received by this
# callback, to sign the request for an access token.
at, ats = service.get_access_token(
cookie['rt'],
cookie['rts'],
params={'oauth_verifier': request.query['oauth_verifier']})
# Store the access token and secret in the client's cookie, replacing the
# request token and secret (which are no longer valid). The access token
# and secret will be valid forever, unless the client revokes them.
set_cookie({'at': at, 'ats': ats})
# Send the client back to the main page where they can make API requests
# using their access token and secret.
redirect('/')
@route('/test')
def test():
"""This route is where our client asks us to make a signed API request on
their behalf."""
if not cookie_has_access_token():
# If we don't have an access token, we can't make a signed API request,
# so send the client back to the main page, where they can follow the
# link to reauthorize.
redirect('/')
return
service = get_service()
# Get the client's cookie, which contains the access token and secret that
# we saved earlier.
cookie = request.get_cookie(COOKIE_NAME, secret=SECRET)
# Make a signed request to the API.
session = OAuth1Session(
service.consumer_key, service.consumer_secret,
access_token=cookie['at'], access_token_secret=cookie['ats'])
response.set_header('Content-Type', 'text/html')
json = make_api_request(session, request.query['path'])
# Show the result of the request to the client.
return make_html_page(TEST_FORM + '<pre>' + html.escape(json) + '</pre>')
if __name__ == '__main__':
# Do this now in order to provoke validation errors from config.json.
get_service()
run(host=HOST, port=PORT)
"""This module contains utilities used by the SmugMug OAuth Demo which are not
relevant to understanding the OAuth workflow."""
from bottle import request, response
from urllib.parse import urlparse, parse_qs
import uuid
from common import API_ORIGIN
COOKIE_NAME = 'c'
# This secret is used for signing cookies.
# If you want the cookies to be valid from one run of this program to the next,
# use a static string here (but make sure to choose it randomly).
SECRET = str(uuid.uuid4())
def get_cookie():
"""Get the client's cookie, which is where we store the client's request
token or access token.
Note that the cookie is not sent to SmugMug, and using OAuth does not
require that you use cookies. It is simply the mechanism by which this
particular example application keeps track of the state of each of its
clients.
"""
return request.get_cookie(COOKIE_NAME, secret=SECRET)
def cookie_has_access_token():
"""Does the client's cookie contain an access token?"""
c = get_cookie()
return type(c) is dict and 'at' in c and 'ats' in c
def set_cookie(obj):
"""Set the contents of the client's cookie."""
response.set_cookie(
COOKIE_NAME, obj, secret=SECRET, httponly=True, path='/')
def make_html_page(content):
return '<html><head><title>SmugMug OAuth Demo</title></head><body>' \
+ content + '</body></html>'
TEST_FORM = '''
<form action="/test" method="GET">
<label for="path">API URI Path:</label>
<input type="text" name="path" id="path" value="/api/v2!authuser">
<input type="submit" value="GET">
</form>
'''
def make_api_request(session, input_path):
parsed = urlparse(input_path)
params = parse_qs(parsed.query)
params['_pretty'] = ''
return session.get(
API_ORIGIN + parsed.path,
params=params,
headers={'Accept': 'application/json'}).text
@garrettwilkin
Copy link

Thanks very much!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment