-
Notifications
You must be signed in to change notification settings - Fork 20
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
base: master
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
env/ | ||
selfsign/output | ||
config.ini |
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" ] |
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 | ||
``` |
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( | ||
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( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you need to add semantic validation... (probably here, problably somewhere else):
|
||
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']) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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' | ||
) | ||
) |
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 | ||
|
||
|
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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
FROM ubuntu:18.04 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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" ] |
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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 | ||
|
There was a problem hiding this comment.
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.