Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add python client example #156

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions examples/python/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
env/
selfsign/output
config.ini
12 changes: 12 additions & 0 deletions examples/python/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
FROM ubuntu:18.04

RUN apt-get update -y && apt-get install -y \
python-pip \
python-dev

COPY ./requirements.txt /app/requirements.txt
WORKDIR /app
RUN pip install -r requirements.txt
COPY . /app
ENTRYPOINT [ "python" ]
CMD [ "app.py" ]
20 changes: 20 additions & 0 deletions examples/python/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@

# generate self signed cert
```
# your website that keymaster will accept local.<yourwebsite>
export WEBSITE=example.com
cd ./selfsign
docker build -t selfsign .
docker run -e "WEBSITE=$WEBSITE" -v `pwd`/output:/output selfsign
```

# redirect localhost.<yourwebsite> to 127.0.0.1
sudo bash -c "echo '127.0.0.1 localhost.$WEBSITE' >> /etc/hosts"


# run python server
```
# sudo because we're binding 443
pip3 install -r requirements.txt
sudo python3 app.py
```
139 changes: 139 additions & 0 deletions examples/python/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import configparser
import logging
import os
import sys

from flask import Flask, request, redirect, url_for, make_response
from flask.json import jsonify
from requests_oauthlib import OAuth2Session
import jwt


log = logging.getLogger('oauthlib')
log.addHandler(logging.StreamHandler(sys.stdout))
log.setLevel(logging.DEBUG)

app = Flask(__name__)


config = configparser.ConfigParser()
config.read('config.ini')
client_id = config['DEFAULT']['client_id']
client_secret = config['DEFAULT']['client_secret']
authorization_endpoint = config['DEFAULT']['authorization_endpoint']
token_endpoint = config['DEFAULT']['token_endpoint']
userinfo_endpoint = config['DEFAULT']['userinfo_endpoint']
redirect_uri = config['DEFAULT']['redirect_uri']
jwt_secrets = config['DEFAULT']['jwt_secrets'].split(',')

algorithm = 'HS256'
jwt_cookie_key = 'jwt_token'


def redirect_with_jwt(url, **jwt_args):
"""
Redirect to url.
All keyword args will be signed into a JWT with the response.
"""
resp = make_response(redirect(url))
resp.set_cookie(jwt_cookie_key, jwt.encode(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both the Cookie and the jwt token should have lifetimes. Which you should verify, for the redirection is should not be more than 300s and once authenticated the actual duration it should be the duration of the authentication request or if not possible, something in the order of hours (2-8).

Also if you plan to use the same cookie name for the redirect and the outh presence you should add a field to distinguish these two cases.

jwt_args,

# The first secret is the current signer.
jwt_secrets[0],
algorithm=algorithm
))
return resp


def get_jwt_token(cookies):
"""
Retrieve the jwt token's as a key, value object
from the flask request cookies object.
If no token exists, returns an empty object.
"""
if jwt_cookie_key in request.cookies:

# We take multiple secrets to allow for online secret rotation.
# The first secret is the current signer,
# and the others are potentially still in use, but will
# be rotated out.
for jwt_secret in jwt_secrets:
try:
return jwt.decode(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you need to add semantic validation... (probably here, problably somewhere else):

  1. Does the jwt return makes sense for this request?
  2. Is is still valid?
  3. It it valid yet?

request.cookies[jwt_cookie_key],
jwt_secret,
algorithms=[algorithm]
)
except jwt.exceptions.InvalidTokenError:
continue

return {}


@app.route("/")
def index():
"""
If the user has no valid auth token:
redirect the user/resource owner to the keymaster.
Otherwise:
the user is authenticated.
"""
jwt = get_jwt_token(request.cookies)
if 'oauth_token' in jwt:
# User is authenticated, don't do the auth dance.
client = OAuth2Session(client_id, token=jwt['oauth_token'])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should store the userinfo in the jwt contents, so that you dont pay a round-trip to the oauth2 provider at every request. (this is another reason to distinguish the two cookies)

return jsonify(client.get(userinfo_endpoint).json())

client = OAuth2Session(
client_id,
scope="openid mail profile",
redirect_uri=redirect_uri
)
authorization_url, state = client.authorization_url(authorization_endpoint)

# State is used to prevent CSRF, keep this for later.
return redirect_with_jwt(authorization_url, oauth_state=state)


@app.route("/callback", methods=["GET"])
def callback():
"""
The user has been redirected back from keymaster to your registered
callback URL. With this redirection comes an authorization code included
in the redirect URL. We will use that to obtain an access token.
"""

jwt = get_jwt_token(request.cookies)
if 'oauth_state' not in jwt:
# something is wrong with the state token,
# so redirect back to start over.
return redirect(url_for('.index'))

client = OAuth2Session(
client_id,
state=jwt['oauth_state'],
redirect_uri=redirect_uri
)

token = client.fetch_token(
token_endpoint,
client_secret=client_secret,
authorization_response=request.url
)

# Save the token
return redirect_with_jwt(url_for('.index'), oauth_token=token)


if __name__ == "__main__":
app.secret_key = os.urandom(24)
app.run(
debug=True,
host='0.0.0.0',
port=443,
ssl_context=(
'selfsign/output/localhost.pem',
'selfsign/output/key.pem'
)
)
10 changes: 10 additions & 0 deletions examples/python/config.ini.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[DEFAULT]
client_id = something
client_secret = something2
authorization_endpoint = https://example.com/authorize
token_endpoint = https://example.com/token
userinfo_endpoint = https://example.com/userinfo
redirect_uri = https://<yourwebsite>/callback
jwt_secrets = secret,secret2


3 changes: 3 additions & 0 deletions examples/python/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Flask==1.0.2
requests-oauthlib==1.0.0
PyJWT==1.6.4
8 changes: 8 additions & 0 deletions examples/python/selfsign/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
FROM ubuntu:18.04
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a docker container for a self-signing is overkill. Just add the Bash script.


RUN apt-get update -y && apt-get install -y \
openssl

COPY ./selfsign.sh /selfsign.sh
RUN ["chmod", "+x", "/selfsign.sh"]
ENTRYPOINT [ "/selfsign.sh" ]
15 changes: 15 additions & 0 deletions examples/python/selfsign/selfsign.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/bin/bash
cd /output
DOMAIN=localhost.$WEBSITE
ANY_INTEGER=$RANDOM
openssl genpkey -algorithm RSA -out key.pem -pkeyopt rsa_keygen_bits:2048
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is my guess that you added this because of the old version of openssl on macos, so convert this line into:
openssl genpkey -algorithm RSA -out key.pem -pkeyopt rsa_keygen_bits:2048 || openssl genrsa 2048

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you help me get around the error I get on osx?

+ openssl genpkey -algorithm RSA -out key.pem -pkeyopt rsa_keygen_bits:2048
openssl:Error: 'genpkey' is an invalid command.

openssl req -new -key key.pem -days 1096 -extensions v3_ca -batch -out example.csr -utf8 -subj "/CN=$DOMAIN"

cat <<EOF > openssl.ss.cnf
basicConstraints = CA:FALSE
subjectAltName =DNS:$DOMAIN
extendedKeyUsage =serverAuth
EOF

openssl x509 -req -sha256 -days 3650 -in example.csr -signkey key.pem -set_serial $ANY_INTEGER -extfile openssl.ss.cnf -out localhost.pem