diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f7959c1 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +**/__pycache__ +**/env_ccauth diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f520285 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +__pycache__/ +instance/secret.json +env_* +*.pyc +.DS_Store diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..55eda8e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM python:alpine3.7 + +COPY . /app +WORKDIR /app + +RUN apk add make + +RUN pip install -r requirements.txt + +EXPOSE 5000 +CMD make prod diff --git a/README.md b/README.md new file mode 100644 index 0000000..bf75286 --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +# compsoc committee sso + +## what? + +This is a tool to allow compsoc committee members to sign into internal applications, using our existing google admin platform. + +## why? + +This lets us greatly reduce the overhead of launching new applications, as we can shift account management up to the long-suffering administrator. + +## how? + +This is just a demo application, intended as a starting off point for creating new applications. You'll need to create a project on (ideally CompSoc's) [GCP](https://console.cloud.google.com/), issue a client ID and secret for a web oauth application, and properly configure the callback urls. + +More verbosely: + +1) Log into [GCP](https://console.cloud.google.com), and create a new project by clicking th project header on the title bar and clicking "New Project." Ideally this should be created under the "comp-soc.com" domain. + +2) Once you've created the project, go to the sidebar > APIs & Services > Credentials. You'll need to add routes like so: + + + + +3) You'll also need to enable access to the People API, which is used to retrieve a profile photo and other information. This can be done through the sidebar > APIs & Services > Library portal. + +Once you've done that, you'll need to create your json configuration file in `instance/secret.json`: + +```json +{ + "client_id": "YOUR_GOOGLE_CLIENT_ID", + "client_secret": "YOUR_GOOGLE_CLIENT_SECRET", + "app_secret_key": "SOME_RANDOM_STRING" +} +``` + +Then you should be good to go! Start the server with: + +``` +$ make run +``` + +# who? + +This was written in a fit of procrastination by [@pkage](//kage.dev). diff --git a/app.py b/app.py new file mode 100644 index 0000000..2b6e20c --- /dev/null +++ b/app.py @@ -0,0 +1,77 @@ +from flask import Flask, redirect, url_for, render_template +from flask_dance.contrib.google import make_google_blueprint, google +from oauthlib.oauth2.rfc6749.errors import TokenExpiredError +import requests +import json + +secrets = json.load(open('instance/secret.json', 'r')) + +app = Flask(__name__) +app.secret_key = secrets['app_secret_key'] +blueprint = make_google_blueprint( + client_id=secrets['client_id'], + client_secret=secrets['client_secret'], + scope=['profile', 'email'] +) +app.register_blueprint(blueprint, url_prefix='/login') + +# magic happens here +def email_valid(email): + if email.endswith('@comp-soc.com') or email.endswith('@hacktheburgh.com') or email.endswith('@sigint.mx'): + return True + return False + +@app.route('/') +def index(): + if google.authorized: + return redirect(url_for('profile')) + return render_template('login.html') + +@app.route('/logout') +def logout(): + # retrieve token + token = blueprint.token["access_token"] + + try: + # revoke permission from Google's API + resp = google.post( + "https://accounts.google.com/o/oauth2/revoke", + params={"token": token}, + headers={"Content-Type": "application/x-www-form-urlencoded"} + ) + assert resp.ok, resp.text + except TokenExpiredError as e: + print('token expired, ignoring') + del blueprint.token # Delete OAuth token from storage + return redirect(url_for('index')) + +@app.route('/profile') +def profile(): + if not google.authorized: + return redirect(url_for('google.login')) + # print(dir(google.get)) + # print('google token: {}'.format(google.token[u'access_token'])) + resp = requests.get( + 'https://people.googleapis.com/v1/people/me', + params={ + 'personFields': 'names,emailAddresses,photos' + }, + headers={ + 'Authorization': 'Bearer {}'.format(google.token[u'access_token']) + }) + + person_info = resp.json() + + profile = { + 'email': person_info[u'emailAddresses'][0][u'value'], + 'name': person_info[u'names'][0][u'displayName'], + 'image': person_info[u'photos'][0][u'url'].split('=')[0] # remove the 100px limit (ends with =s100) + } + + return render_template('profile.html', + profile=profile, + valid=email_valid(profile['email']) + ) + +if __name__ == '__main__': + app.run() diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e69de29 diff --git a/docs/credentials.png b/docs/credentials.png new file mode 100644 index 0000000..d23bdf4 Binary files /dev/null and b/docs/credentials.png differ diff --git a/docs/routes.png b/docs/routes.png new file mode 100644 index 0000000..a6b648b Binary files /dev/null and b/docs/routes.png differ diff --git a/makefile b/makefile new file mode 100644 index 0000000..00ee624 --- /dev/null +++ b/makefile @@ -0,0 +1,8 @@ +all: run + + +run: + OAUTHLIB_INSECURE_TRANSPORT=1 OAUTHLIB_RELAX_TOKEN_SCOPE=1 FLASK_DEBUG=1 flask run + +prod: + FLASK_DEBUG=0 flask run diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7f3d144 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +flask +Flask-Dance +requests diff --git a/static/compsoc-icon.png b/static/compsoc-icon.png new file mode 100644 index 0000000..ca64252 Binary files /dev/null and b/static/compsoc-icon.png differ diff --git a/static/index.css b/static/index.css new file mode 100644 index 0000000..e53331d --- /dev/null +++ b/static/index.css @@ -0,0 +1,81 @@ + @import url('https://fonts.googleapis.com/css?family=Merriweather&display=swap'); + +body { + margin: 0; + background-color: #725752; + + font-family: 'Merriweather', serif; +} + +main { + min-height: 100vh; + + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.row { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; +} + +/* -- LOGIN PAGE -- */ + +.login__bg { +} +.login { + color: white; + opacity: 0.7; + + transition: opacity 0.25s cubic-bezier(0,0,0.3,1); + text-decoration: none; +} +.login > span { + margin-right: 0.5em; +} +.login:hover { + opacity: 1; +} + +/* -- PROFILE -- */ + +.profile__bg { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + + color: white; + min-height: 100vh; +} + +.profile__block { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.profile__img { + width: 300px; + height: 300px; + margin-bottom: 1em; +} + +.profile__status { + margin: 1em 0 1em 0; +} + +.profile__status--invalid { + color: #ee6c4d; +} + +.profile__status--valid { + color: #98af57; +} + diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..588acff --- /dev/null +++ b/templates/base.html @@ -0,0 +1,14 @@ + + + +
+