Compare commits
2 Commits
9a70ef5bb1
...
7f434069f4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f434069f4 | ||
|
|
236cadf901 |
@@ -15,16 +15,20 @@ parser.add_argument('name')
|
|||||||
parser.add_argument('value')
|
parser.add_argument('value')
|
||||||
parser.add_argument('type')
|
parser.add_argument('type')
|
||||||
|
|
||||||
|
# verander de variabelen hieronder als je de script handmatig uitvoert
|
||||||
|
|
||||||
dnsserver = os.environ.get('DNS_SERVER')
|
dnsserver = os.environ.get('DNS_SERVER')
|
||||||
dnsserverport = int(os.environ.get('DNS_PORT', default=53))
|
dnsserverport = int(os.environ.get('DNS_PORT', default=53))
|
||||||
debug = bool(os.environ.get('API_DEBUG', default=False))
|
debug = bool(os.environ.get('API_DEBUG', default=False))
|
||||||
|
|
||||||
|
# end change
|
||||||
|
|
||||||
if dnsserver is None:
|
if dnsserver is None:
|
||||||
print('You did not set a DNS_SERVER environ')
|
print('You did not set a DNS_SERVER environ')
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
|
|
||||||
def check_ipv4(ipv4):
|
def check_ipv4(ipv4): # check if given ipv4 address in string is a valid one
|
||||||
try:
|
try:
|
||||||
if type(ip_address(ipv4)) is not IPv4Address: # als ip adress ongelig is
|
if type(ip_address(ipv4)) is not IPv4Address: # als ip adress ongelig is
|
||||||
raise ValueError # raise value error
|
raise ValueError # raise value error
|
||||||
@@ -34,13 +38,7 @@ def check_ipv4(ipv4):
|
|||||||
return True # adres correct
|
return True # adres correct
|
||||||
|
|
||||||
|
|
||||||
def make_fqdn_check(subname, parentdomain):
|
def make_fqdn_check(subname, parentdomain): # make fqdn from given subdomain name, also check if valid
|
||||||
"""
|
|
||||||
De functie moet de subdomain naam met de parent domain samenvoegen en daarna op errors controleren
|
|
||||||
:param subname: hostname
|
|
||||||
:param parentdomain: zone name
|
|
||||||
:return: hostname.domain.tld in string
|
|
||||||
"""
|
|
||||||
fqdn = f'{subname}.{parentdomain}'
|
fqdn = f'{subname}.{parentdomain}'
|
||||||
if True: # do some regex checking if it made a correct fqdn, currently passing and trusting user input!!!
|
if True: # do some regex checking if it made a correct fqdn, currently passing and trusting user input!!!
|
||||||
return fqdn
|
return fqdn
|
||||||
@@ -48,27 +46,27 @@ def make_fqdn_check(subname, parentdomain):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def validate_authorization(req):
|
def validate_authorization(req): # validate authorization header
|
||||||
jwt = req.get('Authorization')
|
jwt = req.get('Authorization')
|
||||||
if jwt is None:
|
if jwt is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
jwt = jwt.split(' ')[1]
|
jwt = jwt.split(' ')[1] # get jwt from header
|
||||||
|
|
||||||
if GoogleOID.check_jwt(jwt)['error'] is False:
|
if GoogleOID.check_jwt(jwt)['error'] is False: # check if jwt is valid
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
class domains(Resource):
|
class domains(Resource):
|
||||||
def get(self):
|
def get(self): # list domains
|
||||||
"""
|
"""
|
||||||
list all domains
|
list all domains
|
||||||
:return: {"domains": ["name":"a.b"], "error":false}
|
:return: {"domains": ["name":"a.b"], "error":false}
|
||||||
"""
|
"""
|
||||||
# Vooralsnog moet je handmatig domains toevoegen aan BIND, dus dit heeft niet veel nut, hardcoded domeinen.
|
# Vooralsnog moet je handmatig domains toevoegen aan BIND, dus dit heeft niet veel nut, hardcoded domeinen.
|
||||||
if validate_authorization(request.headers) is False:
|
if validate_authorization(request.headers) is False: # check authorization
|
||||||
return {"error": True, "reason": "Invalid authorization header"}, 403
|
return {"error": True, "reason": "Invalid authorization header"}, 403
|
||||||
|
|
||||||
return {'domains': ['school.test'], 'error': False}
|
return {'domains': ['school.test'], 'error': False}
|
||||||
@@ -81,7 +79,7 @@ class domain(Resource):
|
|||||||
:param fqdn: domain name
|
:param fqdn: domain name
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
if validate_authorization(request.headers) is False:
|
if validate_authorization(request.headers) is False: # check authorization
|
||||||
return {"error": True, "reason": "Invalid authorization header"}, 403
|
return {"error": True, "reason": "Invalid authorization header"}, 403
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
@@ -94,21 +92,21 @@ class domain(Resource):
|
|||||||
if t != 'A':
|
if t != 'A':
|
||||||
return {"error": True, "reason": "Only A records are supported"}, 400
|
return {"error": True, "reason": "Only A records are supported"}, 400
|
||||||
|
|
||||||
fqdn = make_fqdn_check(n, dmn)
|
fqdn = make_fqdn_check(n, dmn) # combine subdomain with parent domain
|
||||||
if fqdn is False:
|
if fqdn is False:
|
||||||
return {"error": True, "reason": "Invalid subdomain, domain combination"}, 400
|
return {"error": True, "reason": "Invalid subdomain, domain combination"}, 400
|
||||||
|
|
||||||
dns = DnsZone(dmn, dnsserver, 1, dnsserverport)
|
dns = DnsZone(dmn, dnsserver, 1, dnsserverport) # setup dnszone
|
||||||
|
|
||||||
current = dns.check_address(fqdn)
|
current = dns.check_address(fqdn) # check if address exists
|
||||||
|
|
||||||
if current['error'] is True:
|
if current['error'] is True:
|
||||||
if 'not found' not in current['error_text']: # als er een error is wat niet een not found is
|
if 'not found' not in current['error_text']: # als er een error is wat NIET een "not found" is
|
||||||
return {"error": True, "reason": current['error_text']}, 500
|
return {"error": True, "reason": current['error_text']}, 500
|
||||||
else: # record bestond al niet
|
else: # record bestond al niet
|
||||||
return {"error": True, "reason": "Record does not exist"}, 400
|
return {"error": True, "reason": "Record does not exist"}, 400
|
||||||
|
|
||||||
new = dns.clear_address(fqdn)
|
new = dns.clear_address(fqdn) # delete record
|
||||||
|
|
||||||
if new['error'] is True: # als er een error is met het weghalen van dns regel
|
if new['error'] is True: # als er een error is met het weghalen van dns regel
|
||||||
return {"error": True, "reason": new['error_text']}, 500
|
return {"error": True, "reason": new['error_text']}, 500
|
||||||
@@ -121,12 +119,11 @@ class domain(Resource):
|
|||||||
:param dmn: domain name
|
:param dmn: domain name
|
||||||
:return: alle dns A records als keys
|
:return: alle dns A records als keys
|
||||||
"""
|
"""
|
||||||
# de dnszone library kan geen lijst met records opvragen, een andere manier uitzoeken? mongodb bijhouden?
|
if validate_authorization(request.headers) is False: # check authorization
|
||||||
if validate_authorization(request.headers) is False:
|
|
||||||
return {"error": True, "reason": "Invalid authorization header"}, 403
|
return {"error": True, "reason": "Invalid authorization header"}, 403
|
||||||
|
|
||||||
dns = DnsZone(dmn, dnsserver, 1, dnsserverport)
|
dns = DnsZone(dmn, dnsserver, 1, dnsserverport) # setup dnszone
|
||||||
current = dns.list_addresses()
|
current = dns.list_addresses() # get all a records
|
||||||
|
|
||||||
if current['error'] is True:
|
if current['error'] is True:
|
||||||
return {"error": True, "reason": current['error_text']}, 500
|
return {"error": True, "reason": current['error_text']}, 500
|
||||||
@@ -140,7 +137,7 @@ class domain(Resource):
|
|||||||
:param dmn: domain name
|
:param dmn: domain name
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
if validate_authorization(request.headers) is False:
|
if validate_authorization(request.headers) is False: # check authorization
|
||||||
return {"error": True, "reason": "Invalid authorization header"}, 403
|
return {"error": True, "reason": "Invalid authorization header"}, 403
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
@@ -153,7 +150,7 @@ class domain(Resource):
|
|||||||
if t != 'A': # als record type niet A is
|
if t != 'A': # als record type niet A is
|
||||||
return {"error": True, "reason": "Only A records are supported"}, 400
|
return {"error": True, "reason": "Only A records are supported"}, 400
|
||||||
|
|
||||||
if check_ipv4(v) is False:
|
if check_ipv4(v) is False: # check if ip is valid
|
||||||
return {"error": True, "reason": "Value is not correct"}, 400
|
return {"error": True, "reason": "Value is not correct"}, 400
|
||||||
|
|
||||||
fqdn = make_fqdn_check(n, dmn) # maak fqdn
|
fqdn = make_fqdn_check(n, dmn) # maak fqdn
|
||||||
@@ -188,8 +185,9 @@ api.add_resource(domains, '/api/v1/dns/domains')
|
|||||||
api.add_resource(domain, '/api/v1/dns/domain/<dmn>')
|
api.add_resource(domain, '/api/v1/dns/domain/<dmn>')
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
if check_ipv4(dnsserver) is False: # cant use dns in resolver so check if direct ip is given if not
|
if check_ipv4(dnsserver) is False: # cant use domain in resolver so check if direct ip is given if not
|
||||||
dnsserver = resolver.resolve(dnsserver, "A")[0].to_text() # set try to resolve given string
|
dnsserver = resolver.resolve(dnsserver, "A")[0].to_text() # try to resolve given string
|
||||||
|
|
||||||
GoogleOID = GoogleOID()
|
# do not setup dns zone globally because it errors on simultaneous requests
|
||||||
app.run(debug=debug, host='0.0.0.0', port=5001)
|
GoogleOID = GoogleOID() # setup google oid
|
||||||
|
app.run(debug=debug, host='0.0.0.0', port=5001) # run werkzeug
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ class GoogleOID:
|
|||||||
import requests
|
import requests
|
||||||
import jwt
|
import jwt
|
||||||
import os
|
import os
|
||||||
client_secret = os.environ.get('OPENID_SECRET')
|
client_secret = os.environ.get('OPENID_SECRET') # change this to your secret if running manually
|
||||||
if client_secret is None:
|
|
||||||
|
if client_secret is None: # if environ not set
|
||||||
print('No OPENID_SECRET environ')
|
print('No OPENID_SECRET environ')
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
@@ -14,47 +15,42 @@ class GoogleOID:
|
|||||||
self.settings = {'client_id': '954325872153-1v466clrtgg6h4ptt2ne5pgpb9mhilr5.apps.googleusercontent.com',
|
self.settings = {'client_id': '954325872153-1v466clrtgg6h4ptt2ne5pgpb9mhilr5.apps.googleusercontent.com',
|
||||||
'client_secret': client_secret,
|
'client_secret': client_secret,
|
||||||
'callback_uri': 'http://dns.mashallah.nl:5000/login/gcp/callback',
|
'callback_uri': 'http://dns.mashallah.nl:5000/login/gcp/callback',
|
||||||
'key_server': 'https://www.googleapis.com/oauth2/v3/certs'}
|
'key_server': 'https://www.googleapis.com/oauth2/v3/certs'} # global oid settings
|
||||||
|
|
||||||
def settings(self):
|
def settings(self): # make it so that the settings variable is callable
|
||||||
return self.settings
|
return self.settings
|
||||||
|
|
||||||
def get_token_from_code(self, code):
|
def get_token_from_code(self, code): # get usable api token from oauth code
|
||||||
data = {'grant_type': 'authorization_code',
|
data = {'grant_type': 'authorization_code',
|
||||||
'client_id': self.settings['client_id'],
|
'client_id': self.settings['client_id'],
|
||||||
'client_secret': self.settings['client_secret'],
|
'client_secret': self.settings['client_secret'],
|
||||||
'redirect_uri': self.settings['callback_uri'],
|
'redirect_uri': self.settings['callback_uri'],
|
||||||
'code': code}
|
'code': code}
|
||||||
|
|
||||||
r = self.requests.post('https://oauth2.googleapis.com/token', data=data)
|
r = self.requests.post('https://oauth2.googleapis.com/token', data=data) # exchange code for token
|
||||||
|
|
||||||
if r.status_code != 200:
|
if r.status_code != 200: # if not successful
|
||||||
return {'error': True, 'reason': f'Could not exchange code for access key'}
|
return {'error': True, 'reason': f'Could not exchange code for access key'}
|
||||||
|
|
||||||
return {'error': False, 'data': r.json()}
|
return {'error': False, 'data': r.json()}
|
||||||
|
|
||||||
def get_profile_information(self, token):
|
def get_profile_information(self, token): # get google profile with token
|
||||||
headers = {'Authorization': f'Bearer {token}'}
|
headers = {'Authorization': f'Bearer {token}'}
|
||||||
|
|
||||||
r = self.requests.get('https://openidconnect.googleapis.com/v1/userinfo', headers=headers)
|
r = self.requests.get('https://openidconnect.googleapis.com/v1/userinfo', headers=headers)
|
||||||
|
|
||||||
if r.status_code != 200:
|
if r.status_code != 200: # if not successful
|
||||||
return {'error': True, 'reason': 'Could not get profile info'}
|
return {'error': True, 'reason': 'Could not get profile info'}
|
||||||
|
|
||||||
return {'error': False, 'profile': r.json()}
|
return {'error': False, 'profile': r.json()}
|
||||||
|
|
||||||
def check_jwt(self, bearer):
|
def check_jwt(self, bearer): # check if jwt is signed by google and valid
|
||||||
"""
|
jwks_client = self.jwt.PyJWKClient(self.settings['key_server']) # setup jwks client with keyserver
|
||||||
Decodes JWT and checks if it is for us from google
|
signing_key = jwks_client.get_signing_key_from_jwt(bearer).key # extract signing key from jwt
|
||||||
:param bearer: JWT token in base64 format
|
|
||||||
:return: {'error': False, 'data': decoded jwt dict}
|
|
||||||
"""
|
|
||||||
jwks_client = self.jwt.PyJWKClient(self.settings['key_server'])
|
|
||||||
signing_key = jwks_client.get_signing_key_from_jwt(bearer).key
|
|
||||||
|
|
||||||
try:
|
try: # fails if jwt is not valid eg. not signed by google, expired, wrong application
|
||||||
decoded = self.jwt.decode(bearer, signing_key, algorithms=["RS256"], audience=self.settings['client_id'])
|
decoded = self.jwt.decode(bearer, signing_key, algorithms=["RS256"], audience=self.settings['client_id'])
|
||||||
except self.jwt.exceptions.DecodeError:
|
except self.jwt.exceptions.DecodeError: # catch generic error
|
||||||
return {'error': True, 'reason': 'Error decoding JWT'}
|
return {'error': True, 'reason': 'Error decoding JWT'}
|
||||||
|
|
||||||
return {'error': False, 'data': decoded}
|
return {'error': False, 'data': decoded}
|
||||||
|
|||||||
BIN
eindopdracht/dns_gui/.assets/dashboard.png
Normal file
BIN
eindopdracht/dns_gui/.assets/dashboard.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
BIN
eindopdracht/dns_gui/.assets/login.png
Normal file
BIN
eindopdracht/dns_gui/.assets/login.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.4 KiB |
40
eindopdracht/dns_gui/README.md
Normal file
40
eindopdracht/dns_gui/README.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# DNS Manager GUI
|
||||||
|
In deze folder bevind zich alle code omtrent de web-gui voor de DNS manager.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
### Container
|
||||||
|
Als je direct de container gebruikt moet je de environment variables gebruiken die op de docker page staat
|
||||||
|
https://hub.docker.com/r/4grxfq/gui/ vergeet ook poort 5000 niet te openen naar je container
|
||||||
|
|
||||||
|
### Manual install
|
||||||
|
1. verander de variabelen in gui.py die aangepast moeten worden of zet de environment variabelen in je shell
|
||||||
|
2. run ```pip install -r /app/requirements.txt```
|
||||||
|
3. start flask ```python3 gui.py``` de webserver luistert naar poort 5000
|
||||||
|
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
### Index page
|
||||||
|
Op de index page wordt er een inlog form getoont, ook worden hyperlinks getoont naar de dashboard voor als je al
|
||||||
|
ingelogt bent, en een link naar de uitlog pad. De username en password textvakken doen niets en zijn puur cosmetisch,,
|
||||||
|
je kunt alleen inloggen met de klop die je redirect naar de google OID login pagina.
|
||||||
|

|
||||||
|
|
||||||
|
### Dashboard page
|
||||||
|
De dashboard page laat een basale tabel zien met alle A records die op de nameserver ingesteld staan. Bovenin wordt je
|
||||||
|
profiel getoont zoals gekregen van google en opgeslagen in de database.
|
||||||
|
|
||||||
|
Met de knop "Query JWT" kan je de JWT opvragen die google mee heeft gegeven met het inloggen, deze JWT kan je gebruiken om de API handmatig met curl aan te roepen en
|
||||||
|
is verplicht.
|
||||||
|
|
||||||
|
Met de Add SUB form kan je een gebruiker authorizeren om in laten te loggen. Als een gebruikter niet mag inloggen wordt
|
||||||
|
tijdens het inloggen een error getoont met een 21 cijferig code. Die code moet in de Google UUID box worden ingevoerd
|
||||||
|
om de desbetreffende gebruiker authorizatie te geven om in te kunnen loggen.
|
||||||
|
|
||||||
|
De tabel met DNS records spreekt voorzich. Alle bestaande records worden weergeven en de adressen zijn aanpasbaar.
|
||||||
|
Wanneer er een adres aangepast moet worden is dat makkelijk te doen door de waarde aan te wijzigen en Update aan te
|
||||||
|
klikken. Evendeels is het verwijderen van een record ook mogelijk door op Delete te klikken.
|
||||||
|
|
||||||
|
Een nieuwe record kan worden toegevoegd door de subdomain aan te geven met de juiste IPv4 Waarde. Momenteel zijn alleen
|
||||||
|
A records ondersteund. Klik op Add om de record toe te voegen.
|
||||||
|
|
||||||
|

|
||||||
@@ -5,15 +5,20 @@ from mango import Mango
|
|||||||
from openid import *
|
from openid import *
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
# the options below you must change if you want to use a different zone and or want to run the script manually
|
||||||
|
|
||||||
zone = 'school.test'
|
zone = 'school.test'
|
||||||
dns_api = os.environ.get('DNS_API')
|
dns_api = os.environ.get('DNS_API')
|
||||||
nosql_url = os.environ.get('NOSQL_URL')
|
nosql_url = os.environ.get('NOSQL_URL')
|
||||||
nosql_user = os.environ.get('NOSQL_USER')
|
nosql_user = os.environ.get('NOSQL_USER')
|
||||||
nosql_pass = os.environ.get('NOSQL_PASS')
|
nosql_pass = os.environ.get('NOSQL_PASS')
|
||||||
nosql_port = int(os.environ.get('NOSQL_PORT', default=27017))
|
nosql_port = int(os.environ.get('NOSQL_PORT', default=27017)) # get NOSQL_PORT as int
|
||||||
debug = bool(os.environ.get('API_DEBUG', default=False))
|
debug = bool(os.environ.get('API_DEBUG', default=False)) # debug option as bool
|
||||||
|
|
||||||
|
# end editing
|
||||||
|
|
||||||
if dns_api is None or nosql_pass is None or nosql_port is None or nosql_user is None or nosql_url is None:
|
if dns_api is None or nosql_pass is None or nosql_port is None or nosql_user is None or nosql_url is None:
|
||||||
|
# check if vars are not set
|
||||||
print('Missing DNS or NOSQL environs')
|
print('Missing DNS or NOSQL environs')
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
@@ -21,14 +26,14 @@ app = Flask(__name__)
|
|||||||
app.secret_key = secrets.token_hex(22)
|
app.secret_key = secrets.token_hex(22)
|
||||||
|
|
||||||
|
|
||||||
def record_update(name, typ, value, jwt):
|
def record_update(name, typ, value, jwt): # update record via api server
|
||||||
data = {'name': name, 'type': typ, 'value': value}
|
data = {'name': name, 'type': typ, 'value': value}
|
||||||
headers = {'Authorization': f'Bearer {jwt}'}
|
headers = {'Authorization': f'Bearer {jwt}'}
|
||||||
|
|
||||||
r = requests.post(f'{dns_api}/api/v1/dns/domain/{zone}', data=data, headers=headers)
|
r = requests.post(f'{dns_api}/api/v1/dns/domain/{zone}', data=data, headers=headers)
|
||||||
|
|
||||||
if r.status_code != 200:
|
if r.status_code != 200: # if not sucessful
|
||||||
return {'error': True, 'reason': f'Request got status code: {r.status_code}'}
|
return {'error': True, 'reason': f'Request got status code: {r.status_code}'} # return status code from api
|
||||||
|
|
||||||
return {'error': False}
|
return {'error': False}
|
||||||
|
|
||||||
@@ -37,9 +42,10 @@ def records_get(jwt):
|
|||||||
headers = {'Authorization': f'Bearer {jwt}'}
|
headers = {'Authorization': f'Bearer {jwt}'}
|
||||||
|
|
||||||
r = requests.get(f'{dns_api}/api/v1/dns/domain/{zone}', headers=headers)
|
r = requests.get(f'{dns_api}/api/v1/dns/domain/{zone}', headers=headers)
|
||||||
if r.status_code != 200:
|
|
||||||
return {'error': True, 'reason': f'Request got status code: {r.status_code}'}
|
if r.status_code != 200: # if not sucessful
|
||||||
return r.json()['records']
|
return {'error': True, 'reason': f'Request got status code: {r.status_code}'} # return status code from api
|
||||||
|
return r.json()['records'] # return all the records as a dict
|
||||||
|
|
||||||
|
|
||||||
def record_delete(name, jwt):
|
def record_delete(name, jwt):
|
||||||
@@ -47,18 +53,20 @@ def record_delete(name, jwt):
|
|||||||
headers = {'Authorization': f'Bearer {jwt}'}
|
headers = {'Authorization': f'Bearer {jwt}'}
|
||||||
|
|
||||||
r = requests.delete(f'{dns_api}/api/v1/dns/domain/{zone}', data=data, headers=headers)
|
r = requests.delete(f'{dns_api}/api/v1/dns/domain/{zone}', data=data, headers=headers)
|
||||||
if r.status_code != 200:
|
|
||||||
return {'error': True, 'reason': f'Request got status code: {r.status_code}'}
|
if r.status_code != 200: # if not sucessful
|
||||||
|
return {'error': True, 'reason': f'Request got status code: {r.status_code}'} # return status code from api
|
||||||
|
|
||||||
return {'error': False}
|
return {'error': False}
|
||||||
|
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def index():
|
def index(): # base index page
|
||||||
return render_template('index.html')
|
return render_template('index.html')
|
||||||
|
|
||||||
|
|
||||||
@app.route('/login/gcp/start')
|
@app.route('/login/gcp/start')
|
||||||
def login_start():
|
def login_start(): # client gets redirected to google login page with parameters
|
||||||
nonce = secrets.token_hex(16)
|
nonce = secrets.token_hex(16)
|
||||||
return redirect(f'https://accounts.google.com/o/oauth2/v2/auth?'
|
return redirect(f'https://accounts.google.com/o/oauth2/v2/auth?'
|
||||||
f'response_type=code'
|
f'response_type=code'
|
||||||
@@ -69,100 +77,90 @@ def login_start():
|
|||||||
|
|
||||||
|
|
||||||
@app.route('/login/gcp/callback')
|
@app.route('/login/gcp/callback')
|
||||||
def login_callback():
|
def login_callback(): # client gets returned from google with parameters
|
||||||
"""
|
code = request.args.get('code') # get the code given by google
|
||||||
We get parameters:(code)
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
code = request.args.get('code')
|
|
||||||
|
|
||||||
if code is None:
|
if code is None:
|
||||||
return 'Did not get correct parameters. Missing code'
|
return 'Did not get correct parameters. Missing code'
|
||||||
|
|
||||||
grant = GoogleOID.get_token_from_code(code)
|
grant = GoogleOID.get_token_from_code(code) # exchange code for access token
|
||||||
|
|
||||||
if grant['error'] is True:
|
if grant['error'] is True:
|
||||||
return f"Could not exchange code for token: {grant['reason']}"
|
return f"Could not exchange code for token: {grant['reason']}"
|
||||||
|
|
||||||
profile = GoogleOID.get_profile_information(grant['data']['access_token'])
|
profile = GoogleOID.get_profile_information(grant['data']['access_token']) # get profile info from google
|
||||||
|
|
||||||
if profile['error'] is True:
|
if profile['error'] is True:
|
||||||
return f"Could not get profile information: {profile['reason']}"
|
return f"Could not get profile information: {profile['reason']}"
|
||||||
|
|
||||||
if db.google_check_sso_uuid(profile['profile']['sub'])['error'] is True:
|
if db.google_check_sso_uuid(profile['profile']['sub'])['error'] is True: # check if user may login
|
||||||
return f"Account is unavailable for login {profile['profile']['sub']}"
|
return f"Account is unavailable for login {profile['profile']['sub']}"
|
||||||
|
|
||||||
db.google_update_profile(profile['profile'])
|
db.google_update_profile(profile['profile']) # update google profile in db
|
||||||
db.google_update_lastlogin(profile['profile']['sub'])
|
db.google_update_lastlogin(profile['profile']['sub']) # change login date in db
|
||||||
db.google_overwrite_jwt(profile['profile']['sub'], grant['data']['id_token'])
|
db.google_overwrite_jwt(profile['profile']['sub'], grant['data']['id_token']) # overwrite jwt in db
|
||||||
|
|
||||||
session['username'] = profile['profile']['sub']
|
session['username'] = profile['profile']['sub'] # set flask session so user gets logged in
|
||||||
|
|
||||||
return redirect(url_for('dashboard'))
|
return redirect(url_for('dashboard')) # redirect to dashboard
|
||||||
|
|
||||||
|
|
||||||
@app.route('/dashboard', methods=['GET', 'POST'])
|
@app.route('/dashboard', methods=['GET', 'POST'])
|
||||||
def dashboard():
|
def dashboard(): # dns manager dashboard
|
||||||
if 'username' not in session:
|
if 'username' not in session: # check if flask session is set(user logged in)
|
||||||
return 'You are not logged in <a href="/">login</a>'
|
return 'You are not logged in <a href="/">login</a>'
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST': # if form posted
|
||||||
na = request.form.get('name')
|
na = request.form.get('name')
|
||||||
ty = request.form.get('type')
|
ty = request.form.get('type')
|
||||||
va = request.form.get('value')
|
va = request.form.get('value')
|
||||||
rq = request.form.get('request')
|
rq = request.form.get('request')
|
||||||
|
|
||||||
if rq != "Add" and rq != "Delete" and rq != "Update" and rq != "Query JWT" and rq != "Add SUB":
|
if rq != "Add" and rq != "Delete" and rq != "Update" and rq != "Query JWT" and rq != "Add SUB":
|
||||||
|
# data is missing
|
||||||
return 'Invalid request, did you use the dashboard?'
|
return 'Invalid request, did you use the dashboard?'
|
||||||
|
|
||||||
if rq == 'Add' or rq == 'Update':
|
if rq == 'Add' or rq == 'Update': # if user adds or updates a record
|
||||||
jwt = db.google_get_jwt(session['username'])
|
jwt = db.google_get_jwt(session['username']) # get jwt from db
|
||||||
|
|
||||||
response = record_update(name=na, typ=ty, value=va, jwt=jwt)
|
response = record_update(name=na, typ=ty, value=va, jwt=jwt) # update record
|
||||||
|
|
||||||
if response['error'] is True:
|
if response['error'] is True: # if error with update record
|
||||||
return f"Error processing request: {response['reason']}"
|
return f"Error processing request: {response['reason']}"
|
||||||
|
|
||||||
elif rq == 'Delete':
|
elif rq == 'Delete':
|
||||||
jwt = db.google_get_jwt(session['username'])
|
jwt = db.google_get_jwt(session['username']) # get jwt from db
|
||||||
|
|
||||||
response = record_delete(name=na, jwt=jwt)
|
response = record_delete(name=na, jwt=jwt) # delete record
|
||||||
|
|
||||||
if response['error'] is True:
|
if response['error'] is True: # if error with delete record
|
||||||
return f"Error processing request: {response['reason']}"
|
return f"Error processing request: {response['reason']}"
|
||||||
|
|
||||||
elif rq == "Query JWT":
|
elif rq == "Query JWT": # if user requests jwt from db
|
||||||
jwt = db.google_get_jwt(session['username'])
|
jwt = db.google_get_jwt(session['username']) # get jwt from db
|
||||||
flash(jwt)
|
flash(jwt) # flash jwt message
|
||||||
|
|
||||||
elif rq == "Add SUB":
|
elif rq == "Add SUB": # user adds a sub
|
||||||
db.google_add_new_sub(va)
|
db.google_add_new_sub(va) # add sub to db
|
||||||
|
|
||||||
uuid = session['username']
|
# the functions below are always returned even if POST is used this will keep the page the same after a POST request
|
||||||
profile = db.google_get_profile(uuid)
|
|
||||||
lastlogin = db.google_get_lastlogin(uuid)
|
uuid = session['username'] # get uuid from session
|
||||||
jwt = db.google_get_jwt(uuid)
|
profile = db.google_get_profile(uuid) # get profile from db
|
||||||
records = records_get(jwt)
|
lastlogin = db.google_get_lastlogin(uuid) # get lastlogin from db
|
||||||
|
jwt = db.google_get_jwt(uuid) # get jwt from db
|
||||||
|
records = records_get(jwt) # request records from dns api
|
||||||
|
|
||||||
return render_template('dashboard.html', profile=profile, records=records, lastlogin=lastlogin)
|
return render_template('dashboard.html', profile=profile, records=records, lastlogin=lastlogin)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/logout')
|
@app.route('/logout')
|
||||||
def logout():
|
def logout(): # user logs out
|
||||||
session.pop('username', None)
|
session.pop('username', None) # remove flask session
|
||||||
return redirect(url_for('index'))
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
|
||||||
@app.route('/login/gcp/addsub', methods=['POST'])
|
|
||||||
def addsub():
|
|
||||||
if 'username' not in session:
|
|
||||||
return 'You are not logged in <a href="/">login</a>'
|
|
||||||
|
|
||||||
db.google_add_new_sub(request.form.get('sub'))
|
|
||||||
return 'sub toegevoegd, gebruiker mag volgende keer inloggen'
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
db = Mango(nosql_user, nosql_pass, nosql_url, nosql_port)
|
db = Mango(nosql_user, nosql_pass, nosql_url, nosql_port) # setup mango
|
||||||
GoogleOID = GoogleOID()
|
GoogleOID = GoogleOID() # initialize googleoid
|
||||||
app.run(debug=debug, host='0.0.0.0')
|
app.run(debug=debug, host='0.0.0.0') # run werkzeug
|
||||||
|
|||||||
@@ -10,15 +10,15 @@ class Mango:
|
|||||||
except ConnectionError:
|
except ConnectionError:
|
||||||
print('MongoDB connection error')
|
print('MongoDB connection error')
|
||||||
|
|
||||||
def google_check_sso_uuid(self, uuid):
|
def google_check_sso_uuid(self, uuid): # checks if uuid exist in any document
|
||||||
found = self.users.find_one({"sso.google.profile.sub": uuid})
|
found = self.users.find_one({"sso.google.profile.sub": uuid})
|
||||||
|
|
||||||
if found is None:
|
if found is None: # None if nothing found
|
||||||
return {"error": True, "reason": "User not found"}
|
return {"error": True, "reason": "User not found"}
|
||||||
else:
|
else:
|
||||||
return {"error": False}
|
return {"error": False}
|
||||||
|
|
||||||
def google_update_lastlogin(self, uuid):
|
def google_update_lastlogin(self, uuid): # replaces lastlogin with current time for given user
|
||||||
found = self.users.find_one({"sso.google.profile.sub": uuid})
|
found = self.users.find_one({"sso.google.profile.sub": uuid})
|
||||||
|
|
||||||
if found is None:
|
if found is None:
|
||||||
@@ -30,7 +30,7 @@ class Mango:
|
|||||||
|
|
||||||
return {"error": False}
|
return {"error": False}
|
||||||
|
|
||||||
def google_update_profile(self, profile):
|
def google_update_profile(self, profile): # overwrites user profile with the one that google has given
|
||||||
found = self.users.find_one({"sso.google.profile.sub": profile['sub']})
|
found = self.users.find_one({"sso.google.profile.sub": profile['sub']})
|
||||||
|
|
||||||
found['sso']['google']['profile'] = profile
|
found['sso']['google']['profile'] = profile
|
||||||
@@ -39,23 +39,23 @@ class Mango:
|
|||||||
|
|
||||||
return {"error": False}
|
return {"error": False}
|
||||||
|
|
||||||
def google_get_profile(self, uuid):
|
def google_get_profile(self, uuid): # returns google profile as stored in db for a given user
|
||||||
found = self.users.find_one(({"sso.google.profile.sub": uuid}))
|
found = self.users.find_one(({"sso.google.profile.sub": uuid}))
|
||||||
return found['sso']['google']['profile']
|
return found['sso']['google']['profile']
|
||||||
|
|
||||||
def google_get_lastlogin(self, uuid):
|
def google_get_lastlogin(self, uuid): # returns lastlogin in pretty format for given user
|
||||||
found = self.users.find_one(({"sso.google.profile.sub": uuid}))
|
found = self.users.find_one(({"sso.google.profile.sub": uuid}))
|
||||||
return found['sso']['google']['lastlogin'].strftime('%A %d-%m-%Y, %H:%M:%S')
|
return found['sso']['google']['lastlogin'].strftime('%A %d-%m-%Y, %H:%M:%S')
|
||||||
|
|
||||||
def google_add_new_sub(self, uuid):
|
def google_add_new_sub(self, uuid): # adds new document in db with only the sub for a given uuid
|
||||||
self.users.insert_one({'sso':{'google':{'profile':{"sub": str(uuid)}}}})
|
self.users.insert_one({'sso':{'google':{'profile':{"sub": str(uuid)}}}})
|
||||||
|
|
||||||
def google_overwrite_jwt(self, uuid, jwt):
|
def google_overwrite_jwt(self, uuid, jwt): # overwrite jwt in db for given user
|
||||||
found = self.users.find_one({"sso.google.profile.sub": uuid})
|
found = self.users.find_one({"sso.google.profile.sub": uuid})
|
||||||
|
|
||||||
found['sso']['google']['jwt'] = jwt
|
found['sso']['google']['jwt'] = jwt
|
||||||
self.users.replace_one({"sso.google.profile.sub": uuid}, found)
|
self.users.replace_one({"sso.google.profile.sub": uuid}, found)
|
||||||
|
|
||||||
def google_get_jwt(self, uuid):
|
def google_get_jwt(self, uuid): # return jwt as stored in db for given user
|
||||||
found = self.users.find_one({"sso.google.profile.sub": uuid})
|
found = self.users.find_one({"sso.google.profile.sub": uuid})
|
||||||
return found['sso']['google']['jwt']
|
return found['sso']['google']['jwt']
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ class GoogleOID:
|
|||||||
import requests
|
import requests
|
||||||
import jwt
|
import jwt
|
||||||
import os
|
import os
|
||||||
client_secret = os.environ.get('OPENID_SECRET')
|
client_secret = os.environ.get('OPENID_SECRET') # change this to your secret if running manually
|
||||||
if client_secret is None:
|
|
||||||
|
if client_secret is None: # if environ not set
|
||||||
print('No OPENID_SECRET environ')
|
print('No OPENID_SECRET environ')
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
@@ -14,47 +15,42 @@ class GoogleOID:
|
|||||||
self.settings = {'client_id': '954325872153-1v466clrtgg6h4ptt2ne5pgpb9mhilr5.apps.googleusercontent.com',
|
self.settings = {'client_id': '954325872153-1v466clrtgg6h4ptt2ne5pgpb9mhilr5.apps.googleusercontent.com',
|
||||||
'client_secret': client_secret,
|
'client_secret': client_secret,
|
||||||
'callback_uri': 'http://dns.mashallah.nl:5000/login/gcp/callback',
|
'callback_uri': 'http://dns.mashallah.nl:5000/login/gcp/callback',
|
||||||
'key_server': 'https://www.googleapis.com/oauth2/v3/certs'}
|
'key_server': 'https://www.googleapis.com/oauth2/v3/certs'} # global oid settings
|
||||||
|
|
||||||
def settings(self):
|
def settings(self): # make it so that the settings variable is callable
|
||||||
return self.settings
|
return self.settings
|
||||||
|
|
||||||
def get_token_from_code(self, code):
|
def get_token_from_code(self, code): # get usable api token from oauth code
|
||||||
data = {'grant_type': 'authorization_code',
|
data = {'grant_type': 'authorization_code',
|
||||||
'client_id': self.settings['client_id'],
|
'client_id': self.settings['client_id'],
|
||||||
'client_secret': self.settings['client_secret'],
|
'client_secret': self.settings['client_secret'],
|
||||||
'redirect_uri': self.settings['callback_uri'],
|
'redirect_uri': self.settings['callback_uri'],
|
||||||
'code': code}
|
'code': code}
|
||||||
|
|
||||||
r = self.requests.post('https://oauth2.googleapis.com/token', data=data)
|
r = self.requests.post('https://oauth2.googleapis.com/token', data=data) # exchange code for token
|
||||||
|
|
||||||
if r.status_code != 200:
|
if r.status_code != 200: # if not successful
|
||||||
return {'error': True, 'reason': f'Could not exchange code for access key'}
|
return {'error': True, 'reason': f'Could not exchange code for access key'}
|
||||||
|
|
||||||
return {'error': False, 'data': r.json()}
|
return {'error': False, 'data': r.json()}
|
||||||
|
|
||||||
def get_profile_information(self, token):
|
def get_profile_information(self, token): # get google profile with token
|
||||||
headers = {'Authorization': f'Bearer {token}'}
|
headers = {'Authorization': f'Bearer {token}'}
|
||||||
|
|
||||||
r = self.requests.get('https://openidconnect.googleapis.com/v1/userinfo', headers=headers)
|
r = self.requests.get('https://openidconnect.googleapis.com/v1/userinfo', headers=headers)
|
||||||
|
|
||||||
if r.status_code != 200:
|
if r.status_code != 200: # if not successful
|
||||||
return {'error': True, 'reason': 'Could not get profile info'}
|
return {'error': True, 'reason': 'Could not get profile info'}
|
||||||
|
|
||||||
return {'error': False, 'profile': r.json()}
|
return {'error': False, 'profile': r.json()}
|
||||||
|
|
||||||
def check_jwt(self, bearer):
|
def check_jwt(self, bearer): # check if jwt is signed by google and valid
|
||||||
"""
|
jwks_client = self.jwt.PyJWKClient(self.settings['key_server']) # setup jwks client with keyserver
|
||||||
Decodes JWT and checks if it is for us from google
|
signing_key = jwks_client.get_signing_key_from_jwt(bearer).key # extract signing key from jwt
|
||||||
:param bearer: JWT token in base64 format
|
|
||||||
:return: {'error': False, 'data': decoded jwt dict}
|
|
||||||
"""
|
|
||||||
jwks_client = self.jwt.PyJWKClient(self.settings['key_server'])
|
|
||||||
signing_key = jwks_client.get_signing_key_from_jwt(bearer).key
|
|
||||||
|
|
||||||
try:
|
try: # fails if jwt is not valid eg. not signed by google, expired, wrong application
|
||||||
decoded = self.jwt.decode(bearer, signing_key, algorithms=["RS256"], audience=self.settings['client_id'])
|
decoded = self.jwt.decode(bearer, signing_key, algorithms=["RS256"], audience=self.settings['client_id'])
|
||||||
except self.jwt.exceptions.DecodeError:
|
except self.jwt.exceptions.DecodeError: # catch generic error
|
||||||
return {'error': True, 'reason': 'Error decoding JWT'}
|
return {'error': True, 'reason': 'Error decoding JWT'}
|
||||||
|
|
||||||
return {'error': False, 'data': decoded}
|
return {'error': False, 'data': decoded}
|
||||||
|
|||||||
Reference in New Issue
Block a user